@aihubspot/agent-trade-mcp 0.1.9 → 0.1.10

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.
Files changed (2) hide show
  1. package/dist/index.js +65 -49
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -56,6 +56,9 @@ function loadStoredCredentials(apiKey, secretKey) {
56
56
 
57
57
  // ../core/src/confirmation.ts
58
58
  import { createHash as createHash2, randomUUID } from "crypto";
59
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
60
+ import { homedir } from "os";
61
+ import { join } from "path";
59
62
  function stableJson(value) {
60
63
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
61
64
  if (value && typeof value === "object") {
@@ -66,6 +69,37 @@ function stableJson(value) {
66
69
  function hash(input) {
67
70
  return createHash2("sha256").update(stableJson({ action: input.action, payload: input.payload, context: input.context })).digest("hex");
68
71
  }
72
+ function createPending(input, ttlMs, now) {
73
+ return {
74
+ ...input,
75
+ confirmationId: randomUUID(),
76
+ requestHash: hash(input),
77
+ expiresAtMs: now() + ttlMs
78
+ };
79
+ }
80
+ function preparedAction(pending) {
81
+ return {
82
+ confirmationId: pending.confirmationId,
83
+ expiresAt: new Date(pending.expiresAtMs).toISOString(),
84
+ requestHash: pending.requestHash,
85
+ action: pending.action,
86
+ summary: pending.summary,
87
+ requiresNewUserConfirmation: true,
88
+ nextStep: "Stop and wait for a new explicit user confirmation message. Do not confirm from the same user instruction that prepared this request."
89
+ };
90
+ }
91
+ function confirmPending(pending, userConfirmation, context, now) {
92
+ if (typeof userConfirmation !== "string" || !userConfirmation.trim()) {
93
+ throw new AiHubError("AI_HUB_CONFIRMATION_REQUIRED", "A non-empty new explicit user confirmation message is required before a state-changing request can execute.");
94
+ }
95
+ if (pending.expiresAtMs < now()) {
96
+ throw new AiHubError("AI_HUB_CONFIRMATION_EXPIRED", "Confirmation has expired.");
97
+ }
98
+ if (stableJson(pending.context) !== stableJson(context)) {
99
+ throw new AiHubError("AI_HUB_CONFIRMATION_CONTEXT_CHANGED", "Profile, OpenAPI base URL, configuration, or credentials changed after prepare.");
100
+ }
101
+ return { action: pending.action, payload: pending.payload, requestHash: pending.requestHash };
102
+ }
69
103
  var ConfirmationService = class {
70
104
  constructor(ttlMs = 5 * 60 * 1e3, now = () => Date.now()) {
71
105
  this.ttlMs = ttlMs;
@@ -75,19 +109,9 @@ var ConfirmationService = class {
75
109
  now;
76
110
  actions = /* @__PURE__ */ new Map();
77
111
  prepare(input) {
78
- const expiresAtMs = this.now() + this.ttlMs;
79
- const confirmationId = randomUUID();
80
- const requestHash = hash(input);
81
- this.actions.set(confirmationId, { ...input, confirmationId, requestHash, expiresAtMs, consumed: false });
82
- return {
83
- confirmationId,
84
- expiresAt: new Date(expiresAtMs).toISOString(),
85
- requestHash,
86
- action: input.action,
87
- summary: input.summary,
88
- requiresNewUserConfirmation: true,
89
- nextStep: "Stop and wait for a new explicit user confirmation message. Do not call confirm_action from the same user instruction that prepared this request."
90
- };
112
+ const pending = createPending(input, this.ttlMs, this.now);
113
+ this.actions.set(pending.confirmationId, { ...pending, consumed: false });
114
+ return preparedAction(pending);
91
115
  }
92
116
  confirm(confirmationId, userConfirmation, context) {
93
117
  if (typeof userConfirmation !== "string" || !userConfirmation.trim()) {
@@ -96,17 +120,9 @@ var ConfirmationService = class {
96
120
  const pending = this.actions.get(confirmationId);
97
121
  if (!pending) throw new AiHubError("AI_HUB_CONFIRMATION_NOT_FOUND", "Confirmation was not found or has already been consumed.");
98
122
  if (pending.consumed) throw new AiHubError("AI_HUB_CONFIRMATION_CONSUMED", "Confirmation has already been consumed.");
99
- if (pending.expiresAtMs < this.now()) {
100
- this.actions.delete(confirmationId);
101
- throw new AiHubError("AI_HUB_CONFIRMATION_EXPIRED", "Confirmation has expired.");
102
- }
103
- if (stableJson(pending.context) !== stableJson(context)) {
104
- this.actions.delete(confirmationId);
105
- throw new AiHubError("AI_HUB_CONFIRMATION_CONTEXT_CHANGED", "Profile, OpenAPI base URL, configuration, or credentials changed after prepare.");
106
- }
107
123
  pending.consumed = true;
108
124
  this.actions.delete(confirmationId);
109
- return { action: pending.action, payload: pending.payload, requestHash: pending.requestHash };
125
+ return confirmPending(pending, userConfirmation, context, this.now);
110
126
  }
111
127
  };
112
128
 
@@ -394,16 +410,16 @@ var AiHubSpotApi = class {
394
410
  // ../core/src/config.ts
395
411
  import { createHash as createHash3 } from "crypto";
396
412
  import { lookup } from "dns/promises";
397
- import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
398
- import { homedir } from "os";
399
- import { dirname, join } from "path";
413
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "fs/promises";
414
+ import { homedir as homedir2 } from "os";
415
+ import { dirname, join as join2 } from "path";
400
416
  import { isIP } from "net";
401
417
  import { parse, stringify } from "smol-toml";
402
418
  var CONFIG_DIRECTORY = ".ai-hub";
403
419
  var CONFIG_FILENAME = "config.toml";
404
420
  var PROFILE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
405
- function configFilePath(home = homedir()) {
406
- return join(home, CONFIG_DIRECTORY, CONFIG_FILENAME);
421
+ function configFilePath(home = homedir2()) {
422
+ return join2(home, CONFIG_DIRECTORY, CONFIG_FILENAME);
407
423
  }
408
424
  function validateProfileName(name) {
409
425
  if (!PROFILE_NAME.test(name)) {
@@ -467,7 +483,7 @@ var ConfigStore = class {
467
483
  filePath;
468
484
  async read() {
469
485
  try {
470
- return parseConfig(await readFile(this.filePath, "utf8"));
486
+ return parseConfig(await readFile2(this.filePath, "utf8"));
471
487
  } catch (error) {
472
488
  if (error.code === "ENOENT") return defaultConfig();
473
489
  throw error;
@@ -538,13 +554,13 @@ var ConfigStore = class {
538
554
  }
539
555
  async write(config) {
540
556
  const directory = dirname(this.filePath);
541
- await mkdir(directory, { recursive: true, mode: 448 });
542
- await chmod(directory, 448);
557
+ await mkdir2(directory, { recursive: true, mode: 448 });
558
+ await chmod2(directory, 448);
543
559
  const temporaryPath = `${this.filePath}.${process.pid}.tmp`;
544
- await writeFile(temporaryPath, stringify(config), { mode: 384 });
545
- await chmod(temporaryPath, 384);
546
- await rename(temporaryPath, this.filePath);
547
- await chmod(this.filePath, 384);
560
+ await writeFile2(temporaryPath, stringify(config), { mode: 384 });
561
+ await chmod2(temporaryPath, 384);
562
+ await rename2(temporaryPath, this.filePath);
563
+ await chmod2(this.filePath, 384);
548
564
  }
549
565
  };
550
566
 
@@ -1235,9 +1251,9 @@ function summarizeKlines(value, symbol, interval) {
1235
1251
 
1236
1252
  // ../core/src/tools/symbol-rules.ts
1237
1253
  import { createHash as createHash4 } from "crypto";
1238
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename as rename2, rm, writeFile as writeFile2 } from "fs/promises";
1239
- import { homedir as homedir2 } from "os";
1240
- import { join as join2 } from "path";
1254
+ import { chmod as chmod3, mkdir as mkdir3, readFile as readFile3, rename as rename3, rm as rm2, writeFile as writeFile3 } from "fs/promises";
1255
+ import { homedir as homedir3 } from "os";
1256
+ import { join as join3 } from "path";
1241
1257
  var CACHE_TTL_MS = 60 * 60 * 1e3;
1242
1258
  var cache = /* @__PURE__ */ new Map();
1243
1259
  var pendingLoads = /* @__PURE__ */ new Map();
@@ -1248,16 +1264,16 @@ function persistentCacheEnabled() {
1248
1264
  return process.env.AI_HUB_DISABLE_PERSISTENT_CACHE !== "1";
1249
1265
  }
1250
1266
  function persistentCacheDirectory() {
1251
- return process.env.AI_HUB_CACHE_DIR ?? join2(homedir2(), ".ai-hub", "cache");
1267
+ return process.env.AI_HUB_CACHE_DIR ?? join3(homedir3(), ".ai-hub", "cache");
1252
1268
  }
1253
1269
  function persistentCachePath(key) {
1254
1270
  const hash2 = createHash4("sha256").update(key).digest("hex");
1255
- return join2(persistentCacheDirectory(), `symbols-${hash2}.json`);
1271
+ return join3(persistentCacheDirectory(), `symbols-${hash2}.json`);
1256
1272
  }
1257
1273
  async function loadPersistentSnapshot(key) {
1258
1274
  if (!persistentCacheEnabled()) return void 0;
1259
1275
  try {
1260
- const parsed = JSON.parse(await readFile2(persistentCachePath(key), "utf8"));
1276
+ const parsed = JSON.parse(await readFile3(persistentCachePath(key), "utf8"));
1261
1277
  if (typeof parsed.expiresAt !== "number" || !Number.isFinite(parsed.expiresAt) || parsed.expiresAt <= Date.now() || !("response" in parsed)) return void 0;
1262
1278
  return { expiresAt: parsed.expiresAt, response: parsed.response, rules: parseRules(parsed.response) };
1263
1279
  } catch {
@@ -1270,14 +1286,14 @@ async function persistSnapshot(key, entry) {
1270
1286
  const filePath = persistentCachePath(key);
1271
1287
  const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
1272
1288
  try {
1273
- await mkdir2(directory, { recursive: true, mode: 448 });
1274
- await chmod2(directory, 448);
1275
- await writeFile2(temporaryPath, JSON.stringify({ expiresAt: entry.expiresAt, response: entry.response }), { mode: 384 });
1276
- await chmod2(temporaryPath, 384);
1277
- await rename2(temporaryPath, filePath);
1278
- await chmod2(filePath, 384);
1289
+ await mkdir3(directory, { recursive: true, mode: 448 });
1290
+ await chmod3(directory, 448);
1291
+ await writeFile3(temporaryPath, JSON.stringify({ expiresAt: entry.expiresAt, response: entry.response }), { mode: 384 });
1292
+ await chmod3(temporaryPath, 384);
1293
+ await rename3(temporaryPath, filePath);
1294
+ await chmod3(filePath, 384);
1279
1295
  } catch {
1280
- await rm(temporaryPath, { force: true }).catch(() => void 0);
1296
+ await rm2(temporaryPath, { force: true }).catch(() => void 0);
1281
1297
  }
1282
1298
  }
1283
1299
  function normalizedSymbol2(symbol) {
@@ -2511,7 +2527,7 @@ var ToolRegistry = class {
2511
2527
  if (tool.operation !== "write" || !tool.writeSummary) throw new AiHubError("AI_HUB_TOOL_NOT_WRITE", `Tool "${name}" is not a confirmable write Tool.`);
2512
2528
  const validInput = tool.validate(input);
2513
2529
  const preflightInput = tool.preflight ? await tool.preflight(validInput, context) : validInput;
2514
- return confirmations.prepare({
2530
+ return await confirmations.prepare({
2515
2531
  action: tool.name,
2516
2532
  payload: preflightInput,
2517
2533
  context: confirmationContext(context),
@@ -2811,7 +2827,7 @@ function createServer(profileName, readOnly) {
2811
2827
  const registry = createToolRegistry();
2812
2828
  const writeExecutor = new ToolWriteExecutor(registry);
2813
2829
  const server = new Server(
2814
- { name: "ai-hub-agent-trade", version: "0.1.9" },
2830
+ { name: "ai-hub-agent-trade", version: "0.1.10" },
2815
2831
  {
2816
2832
  capabilities: { tools: {} },
2817
2833
  instructions: "Every successful read-tool response provides both JSON text and MCP structuredContent in the envelope { ok: true, data: ... }. All read-tool data is normalized: dataType=array means use data.items and data.count; dataType=object or scalar means use data.value; dataType=null means no value. Inspect data.dataType before formatting and never assume undocumented nested keys. For a generic trading-pair list, call market_get_symbol_overview first; it returns only counts and a small sample. For paged or quote-asset browsing, use market_list_symbols. Use market_search_symbols only when the user gave a keyword; query is required. Use market_get_symbol_info only for exact precision and minimum rules. Do not use a symbol search as a generic all-symbol list. The complete raw symbols payload is intentionally unavailable in MCP. For broad market questions, call market_get_ticker_summary, market_get_depth_summary, market_get_trades_summary, or market_get_klines_summary instead of fetching large raw payloads. Account types are fixed: 1=Spot, 2=Isolated Margin, 3=Cross Margin, 4=C2C, 5=Derivatives. Never call account type 2 Derivatives. For Spot to Derivatives, use account_prepare_transfer with fromAccount=EXCHANGE and toAccount=FUTURE. Use wallet_prepare_universal_transfer for margin or C2C; account type 2 requires its isolated-margin trading pair in symbol. Prepare/confirm tools return their documented action payload directly in data. For every state-changing action, call only a spot_prepare_* or margin_prepare_* tool first and show its exact summary to the user. Stop and wait for a new, explicit user confirmation message. Only then call confirm_action with that new message verbatim in userConfirmation. Never call prepare and confirm consecutively for one user instruction; never infer confirmation from prior intent, silence, or an Agent-generated message. For spot and margin orders, MARKET BUY always uses quoteAmount (the quote asset to spend); MARKET SELL uses baseQuantity (the base asset to sell). Never reinterpret a requested base quantity as quoteAmount."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aihubspot/agent-trade-mcp",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Local stdio MCP server for AI Hub spot trading tools",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {