@aihubspot/agent-trade-mcp 0.1.0 → 0.1.3

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 (3) hide show
  1. package/README.md +21 -0
  2. package/dist/index.js +755 -134
  3. package/package.json +2 -3
package/README.md CHANGED
@@ -3,3 +3,24 @@
3
3
  Install with `npm install -g @aihubspot/agent-trade-mcp`, then start `ai-hub-trade-mcp --profile default`.
4
4
 
5
5
  This package provides a local stdio MCP server. State-changing operations require a preview and a new explicit user confirmation.
6
+
7
+ It reads the same local profile as the CLI from `~/.ai-hub/config.toml`. The API key and secret key are stored as plaintext in that file with mode `600`; configure them once through `ai-hub config set-credentials --profile <name>`. Do not share this file.
8
+
9
+ ## Client setup
10
+
11
+ Register the local stdio MCP server with one supported client. The setup command is the only client-configuration entry point in this phase:
12
+
13
+ ```bash
14
+ ai-hub-trade-mcp setup --client cursor --profile default
15
+ ai-hub-trade-mcp setup --client claude-desktop --profile default
16
+ ai-hub-trade-mcp setup --client claude-code --profile default
17
+ ai-hub-trade-mcp setup --client codex --profile default
18
+ ```
19
+
20
+ Setup registers the currently installed MCP binary through its absolute Node runtime and entrypoint path, so desktop clients do not depend on a global PATH or an unpublished `npx` package. Cursor and Claude Desktop configurations are merged with existing MCP servers through their JSON configurations. Claude Code and Codex are registered through their official CLIs; Claude Code uses user scope. Existing JSON configurations are validated, atomically updated, and backed up before their first modification. The setup command stores no API credentials; the MCP server continues to read its profile from `~/.ai-hub/config.toml`.
21
+
22
+ ## Spot market-order units
23
+
24
+ Use `spot_prepare_market_buy` or `margin_prepare_market_buy` only with `quoteAmount` (for example, the exact USDT amount to spend for `ETHUSDT`). Use `spot_prepare_market_sell` or `margin_prepare_market_sell` only with `baseQuantity` (the exact ETH amount to sell for `ETHUSDT`). A market buy cannot guarantee an exact base-asset quantity; it must never reinterpret a requested base quantity as a quote amount.
25
+
26
+ Before any order preview, MCP lazily loads `/sapi/v2/symbols` once per local profile and caches the symbol rules in memory for five minutes. Known quantity/price precision and limit-order minimum violations are rejected before confirmation.
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import { realpathSync } from "fs";
5
+ import { fileURLToPath } from "url";
4
6
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
7
 
6
8
  // ../core/src/errors.ts
@@ -12,12 +14,30 @@ var AiHubError = class extends Error {
12
14
  this.code = code;
13
15
  }
14
16
  };
17
+ var OpenApiBusinessError = class extends AiHubError {
18
+ constructor(diagnosis) {
19
+ super(
20
+ "AI_HUB_OPENAPI_BUSINESS_ERROR",
21
+ `OpenAPI business error ${diagnosis.upstreamCode}: ${diagnosis.upstreamMessage}`
22
+ );
23
+ this.diagnosis = diagnosis;
24
+ this.name = "OpenApiBusinessError";
25
+ }
26
+ diagnosis;
27
+ };
28
+ function toAiHubErrorPayload(error) {
29
+ if (error instanceof OpenApiBusinessError) {
30
+ return { code: error.code, ...error.diagnosis };
31
+ }
32
+ if (error instanceof AiHubError) {
33
+ return { code: error.code, message: error.message };
34
+ }
35
+ return { code: "AI_HUB_UNEXPECTED_ERROR", message: error instanceof Error ? error.message : "Unexpected error" };
36
+ }
15
37
 
16
38
  // ../core/src/credential.ts
17
39
  import { createHash } from "crypto";
18
- import keytar from "keytar";
19
- var SERVICE_NAME = "ai-hub-agent-trade";
20
- function validate(credentials) {
40
+ function validateApiCredentials(credentials) {
21
41
  const apiKey = credentials.apiKey.trim();
22
42
  const secretKey = credentials.secretKey.trim();
23
43
  if (!apiKey || !secretKey) {
@@ -28,28 +48,11 @@ function validate(credentials) {
28
48
  function versionOf(credentials) {
29
49
  return createHash("sha256").update(`${credentials.apiKey}\0${credentials.secretKey}`).digest("hex");
30
50
  }
31
- var CredentialStore = class {
32
- async set(profile, credentials) {
33
- const value = validate(credentials);
34
- await keytar.setPassword(SERVICE_NAME, profile, JSON.stringify(value));
35
- return { credentialRef: `keychain:${SERVICE_NAME}/${profile}`, credentialVersion: versionOf(value) };
36
- }
37
- async get(profile) {
38
- const stored = await keytar.getPassword(SERVICE_NAME, profile);
39
- if (!stored) return void 0;
40
- let parsed;
41
- try {
42
- parsed = JSON.parse(stored);
43
- } catch {
44
- throw new AiHubError("AI_HUB_CREDENTIAL_INVALID", "Stored credentials are unreadable. Set credentials again.");
45
- }
46
- const credentials = validate(parsed);
47
- return { ...credentials, credentialVersion: versionOf(credentials) };
48
- }
49
- async remove(profile) {
50
- return keytar.deletePassword(SERVICE_NAME, profile);
51
- }
52
- };
51
+ function loadStoredCredentials(apiKey, secretKey) {
52
+ if (apiKey === void 0 && secretKey === void 0) return void 0;
53
+ const credentials = validateApiCredentials({ apiKey: apiKey ?? "", secretKey: secretKey ?? "" });
54
+ return { ...credentials, credentialVersion: versionOf(credentials) };
55
+ }
53
56
 
54
57
  // ../core/src/confirmation.ts
55
58
  import { createHash as createHash2, randomUUID } from "crypto";
@@ -109,6 +112,100 @@ var ConfirmationService = class {
109
112
 
110
113
  // ../core/src/openapi.ts
111
114
  import { createHmac } from "crypto";
115
+
116
+ // ../core/src/openapi-error-catalog.ts
117
+ var globalErrors = {
118
+ "-1002": { reason: "API_KEY_REQUIRED", suggestedAction: "Configure credentials for the selected profile, then retry the read request." },
119
+ "-1003": { reason: "RATE_LIMITED", suggestedAction: "Wait before retrying. Reduce request frequency and do not retry a write automatically.", retryable: true },
120
+ "-1006": { reason: "WRITE_STATUS_UNKNOWN", suggestedAction: "Do not retry automatically. Query the affected order or history record before taking another action.", writeOutcomeUnknown: true },
121
+ "-1007": { reason: "UPSTREAM_TIMEOUT", suggestedAction: "For a write, query the affected order or history record before retrying. A read can be retried after a short delay.", retryable: true, writeOutcomeUnknown: true },
122
+ "-1015": { reason: "TOO_MANY_ORDERS", suggestedAction: "Reduce the number of active or requested orders before retrying." },
123
+ "-1020": { reason: "UNSUPPORTED_OPERATION", suggestedAction: "Use a supported command and do not substitute another operation automatically." },
124
+ "-1021": { reason: "INVALID_TIMESTAMP", suggestedAction: "Synchronize the local system clock, then retry the request." },
125
+ "-1022": { reason: "INVALID_SIGNATURE", suggestedAction: "Verify the selected profile credentials and signing configuration, then retry." },
126
+ "-1023": { reason: "TIMESTAMP_REQUIRED", suggestedAction: "Use the AI Hub CLI or MCP client so the request timestamp is supplied automatically." },
127
+ "-1024": { reason: "SIGNATURE_REQUIRED", suggestedAction: "Use the AI Hub CLI or MCP client so the request is signed automatically." },
128
+ "-1100": { reason: "INVALID_REQUEST_CHARACTERS", suggestedAction: "Correct the invalid request value and retry." },
129
+ "-1102": { reason: "MISSING_OR_MALFORMED_PARAMETER", suggestedAction: "Provide every required parameter in the documented format." },
130
+ "-1103": { reason: "UNKNOWN_PARAMETER", suggestedAction: "Remove unsupported parameters and use the CLI command reference." },
131
+ "-1111": { reason: "INVALID_PRECISION", suggestedAction: "Use the symbol's allowed price and quantity precision." },
132
+ "-1121": { reason: "INVALID_SYMBOL", suggestedAction: "Query supported symbols and use an exact symbol value." },
133
+ "-1136": { reason: "ORDER_QUANTITY_TOO_SMALL", suggestedAction: "Increase the order quantity to the symbol minimum." },
134
+ "-1138": { reason: "ORDER_PRICE_OUT_OF_RANGE", suggestedAction: "Use a price inside the allowed range, then create a new preview." },
135
+ "-1139": { reason: "MARKET_ORDER_UNSUPPORTED", suggestedAction: "Use a supported order type for the selected symbol." },
136
+ "-1145": { reason: "ORDER_CANNOT_BE_CANCELLED", suggestedAction: "Query the latest order status before attempting another cancellation." },
137
+ "-2013": { reason: "ORDER_NOT_FOUND", suggestedAction: "Verify the symbol and order ID, then query open orders if needed." },
138
+ "-2014": { reason: "INVALID_API_KEY_FORMAT", suggestedAction: "Replace the configured API key with a valid value." },
139
+ "-2015": { reason: "API_KEY_REJECTED", suggestedAction: "Verify API key permissions, IP restrictions, and the selected tenant profile." },
140
+ "-2016": { reason: "TRADING_DISABLED", suggestedAction: "The account is restricted. Contact the platform administrator before trying again." },
141
+ "-2017": { reason: "INSUFFICIENT_BALANCE", suggestedAction: "Check the available balance and reduce the requested amount before creating a new preview." },
142
+ "-2018": { reason: "DUPLICATE_WITHDRAWAL_REQUEST", suggestedAction: "Use a new withdrawal order ID or query withdrawal history before retrying." },
143
+ "-2021": { reason: "IDENTITY_VERIFICATION_REQUIRED", suggestedAction: "Complete the account verification required by the tenant before retrying." },
144
+ "-2022": { reason: "LIMIT_ORDER_VOLUME_TOO_SMALL", suggestedAction: "Increase the limit-order volume to the returned minimum." },
145
+ "-2023": { reason: "MARKET_ORDER_VOLUME_TOO_SMALL", suggestedAction: "Increase the market-order sell volume to the returned minimum." },
146
+ "-2024": { reason: "LIMIT_ORDER_PRICE_TOO_SMALL", suggestedAction: "Increase the limit-order price to the returned minimum." },
147
+ "-2025": { reason: "MARKET_ORDER_AMOUNT_TOO_SMALL", suggestedAction: "Increase the market-order buy amount to the returned minimum." },
148
+ "-2026": { reason: "BUY_AMOUNT_TOO_LARGE", suggestedAction: "Reduce the buy amount before creating a new preview." },
149
+ "-2027": { reason: "SELL_AMOUNT_TOO_LARGE", suggestedAction: "Reduce the sell amount before creating a new preview." },
150
+ "-2029": { reason: "LIMIT_ORDER_NOTIONAL_TOO_SMALL", suggestedAction: "Increase the limit-order notional amount to the returned minimum." },
151
+ "-2030": { reason: "TRANSFER_NOT_FOUND", suggestedAction: "Verify the transfer ID, or query by the documented source and destination accounts." },
152
+ "-2031": { reason: "TRANSFER_TYPE_UNSUPPORTED", suggestedAction: "Use a supported source and destination account combination." },
153
+ "-2033": { reason: "ASSET_TRANSFER_FAILED", suggestedAction: "Query transfer history before attempting another transfer." },
154
+ "-2034": { reason: "SUB_ACCOUNT_RELATION_NOT_FOUND", suggestedAction: "Use a sub-account that belongs to the authenticated parent account." },
155
+ "-2035": { reason: "SUB_ACCOUNT_API_KEY_NOT_FOUND", suggestedAction: "Verify the sub-account API key identifier before retrying." },
156
+ "-2036": { reason: "SUB_ACCOUNT_PERMISSION_DENIED", suggestedAction: "Use a parent-account credential with the required sub-account permission." },
157
+ "-2037": { reason: "SUB_ACCOUNT_CREATION_FAILED", suggestedAction: "Check the sub-account limit and requested identifier before creating a new preview." },
158
+ "-2038": { reason: "SUB_ACCOUNT_UPDATE_FAILED", suggestedAction: "Query the latest sub-account state before retrying the update." },
159
+ "-2040": { reason: "SUB_ACCOUNT_CREDENTIAL_REQUIRED", suggestedAction: "Use the sub-account credential required by this endpoint." },
160
+ "10001": { reason: "USER_ACCOUNT_LOCKED", suggestedAction: "The account is restricted. Contact the platform administrator." },
161
+ "10004": { reason: "USER_NOT_AUTHENTICATED", suggestedAction: "Configure valid credentials for the selected profile." },
162
+ "10005": { reason: "PERMISSION_DENIED", suggestedAction: "Verify the API key permissions and the tenant feature permission." },
163
+ "10006": { reason: "TENANT_PERMISSION_DENIED", suggestedAction: "Ask the tenant administrator to enable this capability." },
164
+ "21018": { reason: "SUB_ACCOUNT_NOT_FOUND", suggestedAction: "List sub-accounts and use an existing sub-account ID." },
165
+ "21019": { reason: "COIN_NOT_FOUND", suggestedAction: "Query supported coins and use an exact coin symbol." },
166
+ "21020": { reason: "SYMBOL_NOT_FOUND", suggestedAction: "Query supported symbols and use an exact symbol value." },
167
+ "21022": { reason: "ORDER_NOT_FOUND", suggestedAction: "Verify the order identifier and symbol, then query current orders if needed." },
168
+ "21024": { reason: "SUB_ACCOUNT_FEATURE_PERMISSION_DENIED", suggestedAction: "Enable the required sub-account feature permission before retrying." },
169
+ "21025": { reason: "SUB_ACCOUNT_LIMIT_REACHED", suggestedAction: "The parent account has reached its sub-account limit." }
170
+ };
171
+ var endpointErrors = {
172
+ "/sapi/v1/withdraw/query:10005": { reason: "WITHDRAW_HISTORY_PERMISSION_DENIED", suggestedAction: "Enable the withdrawal-history permission for this API key, then retry." },
173
+ "/sapi/v1/withdraw/query:-2015": { reason: "WITHDRAW_HISTORY_TRUSTED_IP_REQUIRED", suggestedAction: "Add a trusted IP to the API key before querying withdrawal history." },
174
+ "/sapi/v1/withdraw/query:-1004": { reason: "WITHDRAW_HISTORY_PARENT_ACCOUNT_REQUIRED", suggestedAction: "Use a parent-account credential for withdrawal history." },
175
+ "/sapi/v1/sub_user/asset/root_transfer_query:-2036": { reason: "ROOT_SUB_TRANSFER_PARENT_PERMISSION_REQUIRED", suggestedAction: "Use a parent-account credential that can access the requested sub-account." },
176
+ "/sapi/v1/sub_user/asset/transfer_query:-2034": { reason: "SUB_ACCOUNT_INTERNAL_TRANSFER_RELATION_NOT_FOUND", suggestedAction: "Use a sub-account that belongs to the authenticated parent account." },
177
+ "/sapi/v1/asset/subaccount/transfer_query:-2040": { reason: "PARENT_TRANSFER_SUB_ACCOUNT_REQUIRED", suggestedAction: "Run this query with the relevant sub-account credential." }
178
+ };
179
+ function normalizeCode(value) {
180
+ return String(value).trim();
181
+ }
182
+ function diagnoseOpenApiBusinessError(path2, upstreamCode, upstreamMessage) {
183
+ const code = normalizeCode(upstreamCode);
184
+ const definition = endpointErrors[`${path2}:${code}`] ?? globalErrors[code];
185
+ if (!definition) {
186
+ return {
187
+ upstreamCode: code,
188
+ upstreamMessage,
189
+ reason: "UNKNOWN_UPSTREAM_CODE",
190
+ suggestedAction: "Review the upstream message and endpoint. Do not retry a state-changing request automatically.",
191
+ retryable: false,
192
+ writeOutcomeUnknown: false
193
+ };
194
+ }
195
+ return {
196
+ upstreamCode: code,
197
+ upstreamMessage,
198
+ reason: definition.reason,
199
+ suggestedAction: definition.suggestedAction,
200
+ retryable: definition.retryable ?? false,
201
+ writeOutcomeUnknown: definition.writeOutcomeUnknown ?? false
202
+ };
203
+ }
204
+ function isOpenApiSuccessCode(value) {
205
+ return value === 0 || value === "0";
206
+ }
207
+
208
+ // ../core/src/openapi.ts
112
209
  function parseJsonPreservingLargeIntegers(raw) {
113
210
  let transformed = "";
114
211
  let inString = false;
@@ -155,13 +252,13 @@ function queryString(values) {
155
252
  }
156
253
  return params.toString();
157
254
  }
158
- function joinUrl(baseUrl, path, query) {
159
- if (!path.startsWith("/")) throw new AiHubError("AI_HUB_INVALID_PATH", "OpenAPI paths must start with '/'.");
160
- const url = `${baseUrl.replace(/\/+$/, "")}${path}`;
255
+ function joinUrl(baseUrl, path2, query) {
256
+ if (!path2.startsWith("/")) throw new AiHubError("AI_HUB_INVALID_PATH", "OpenAPI paths must start with '/'.");
257
+ const url = `${baseUrl.replace(/\/+$/, "")}${path2}`;
161
258
  return query ? `${url}?${query}` : url;
162
259
  }
163
- function signRequest(timestamp, method, path, secretKey, query, body) {
164
- const payload = `${timestamp}${method.toUpperCase()}${path}${query ? `?${query}` : ""}${body ?? ""}`;
260
+ function signRequest(timestamp, method, path2, secretKey, query, body) {
261
+ const payload = `${timestamp}${method.toUpperCase()}${path2}${query ? `?${query}` : ""}${body ?? ""}`;
165
262
  return createHmac("sha256", secretKey).update(payload).digest("hex");
166
263
  }
167
264
  var AiHubSpotApi = class {
@@ -236,13 +333,13 @@ var AiHubSpotApi = class {
236
333
  * defined by the API document. Keep endpoint selection in the individual Tool
237
334
  * definition; never expose this method directly through CLI or MCP.
238
335
  */
239
- signedGet(path, query, credentials) {
240
- return this.request("GET", path, query, void 0, credentials);
336
+ signedGet(path2, query, credentials) {
337
+ return this.request("GET", path2, query, void 0, credentials);
241
338
  }
242
- signedPost(path, body, credentials) {
243
- return this.request("POST", path, {}, body, credentials);
339
+ signedPost(path2, body, credentials) {
340
+ return this.request("POST", path2, {}, body, credentials);
244
341
  }
245
- async request(method, path, query = {}, body, credentials) {
342
+ async request(method, path2, query = {}, body, credentials) {
246
343
  const encodedQuery = queryString(query);
247
344
  const bodyString = body ? JSON.stringify(body) : void 0;
248
345
  const headers = {
@@ -255,11 +352,11 @@ var AiHubSpotApi = class {
255
352
  const timestamp = Date.now().toString();
256
353
  headers["X-CH-APIKEY"] = credentials.apiKey;
257
354
  headers["X-CH-TS"] = timestamp;
258
- headers["X-CH-SIGN"] = signRequest(timestamp, method, path, credentials.secretKey, encodedQuery || void 0, bodyString);
355
+ headers["X-CH-SIGN"] = signRequest(timestamp, method, path2, credentials.secretKey, encodedQuery || void 0, bodyString);
259
356
  }
260
357
  let response;
261
358
  try {
262
- response = await fetch(joinUrl(this.baseUrl, path, encodedQuery), {
359
+ response = await fetch(joinUrl(this.baseUrl, path2, encodedQuery), {
263
360
  method,
264
361
  headers,
265
362
  body: bodyString,
@@ -277,8 +374,18 @@ var AiHubSpotApi = class {
277
374
  throw new AiHubError("AI_HUB_OPENAPI_HTTP_ERROR", `OpenAPI returned HTTP ${response.status}.`);
278
375
  }
279
376
  try {
280
- return parseJsonPreservingLargeIntegers(raw);
281
- } catch {
377
+ const parsed = parseJsonPreservingLargeIntegers(raw);
378
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
379
+ const payload = parsed;
380
+ if (Object.hasOwn(payload, "code") && !isOpenApiSuccessCode(payload.code)) {
381
+ const upstreamCode = typeof payload.code === "string" || typeof payload.code === "number" ? payload.code : String(payload.code);
382
+ const upstreamMessage = typeof payload.msg === "string" ? payload.msg : "OpenAPI returned a business failure without a message.";
383
+ throw new OpenApiBusinessError(diagnoseOpenApiBusinessError(path2, upstreamCode, upstreamMessage));
384
+ }
385
+ }
386
+ return parsed;
387
+ } catch (error) {
388
+ if (error instanceof OpenApiBusinessError) throw error;
282
389
  throw new AiHubError("AI_HUB_OPENAPI_INVALID_RESPONSE", "OpenAPI returned a non-JSON response.");
283
390
  }
284
391
  }
@@ -377,7 +484,11 @@ var ConfigStore = class {
377
484
  const config = await this.read();
378
485
  const normalizedUrl = await normalizeOpenApiBaseUrl(openApiBaseUrl);
379
486
  const existing = config.profiles[name];
380
- config.profiles[name] = { openapi_base_url: normalizedUrl, ...existing?.credential_ref ? { credential_ref: existing.credential_ref } : {} };
487
+ config.profiles[name] = {
488
+ openapi_base_url: normalizedUrl,
489
+ ...existing?.api_key ? { api_key: existing.api_key } : {},
490
+ ...existing?.secret_key ? { secret_key: existing.secret_key } : {}
491
+ };
381
492
  if (Object.keys(config.profiles).length === 1) config.default_profile = name;
382
493
  await this.write(config);
383
494
  return this.resolveFrom(config, name);
@@ -386,6 +497,12 @@ var ConfigStore = class {
386
497
  const config = await this.read();
387
498
  return this.resolveFrom(config, requestedName ?? config.default_profile);
388
499
  }
500
+ async getCredentials(requestedName) {
501
+ const config = await this.read();
502
+ const name = requestedName ?? config.default_profile;
503
+ const profile = this.profileFrom(config, name);
504
+ return loadStoredCredentials(profile.api_key, profile.secret_key);
505
+ }
389
506
  async removeProfile(name) {
390
507
  validateProfileName(name);
391
508
  const config = await this.read();
@@ -394,26 +511,31 @@ var ConfigStore = class {
394
511
  if (config.default_profile === name) config.default_profile = Object.keys(config.profiles)[0] ?? "default";
395
512
  await this.write(config);
396
513
  }
397
- async setCredentialRef(name, credentialRef) {
514
+ async setCredentials(name, credentials) {
398
515
  validateProfileName(name);
399
516
  const config = await this.read();
400
517
  const profile = config.profiles[name];
401
518
  if (!profile) throw new AiHubError("AI_HUB_PROFILE_NOT_FOUND", `Profile "${name}" does not exist.`);
402
- config.profiles[name] = { ...profile, credential_ref: credentialRef };
519
+ const value = validateApiCredentials(credentials);
520
+ config.profiles[name] = { openapi_base_url: profile.openapi_base_url, api_key: value.apiKey, secret_key: value.secretKey };
403
521
  await this.write(config);
404
522
  return this.resolveFrom(config, name);
405
523
  }
406
524
  resolveFrom(config, name) {
407
525
  validateProfileName(name);
408
- const profile = config.profiles[name];
409
- if (!profile) throw new AiHubError("AI_HUB_PROFILE_NOT_FOUND", `Profile "${name}" does not exist.`);
526
+ const profile = this.profileFrom(config, name);
410
527
  return {
411
528
  name,
412
529
  openApiBaseUrl: profile.openapi_base_url,
413
- credentialRef: profile.credential_ref,
414
530
  configVersion: versionOf2(config, name)
415
531
  };
416
532
  }
533
+ profileFrom(config, name) {
534
+ validateProfileName(name);
535
+ const profile = config.profiles[name];
536
+ if (!profile) throw new AiHubError("AI_HUB_PROFILE_NOT_FOUND", `Profile "${name}" does not exist.`);
537
+ return profile;
538
+ }
417
539
  async write(config) {
418
540
  const directory = dirname(this.filePath);
419
541
  await mkdir(directory, { recursive: true, mode: 448 });
@@ -428,8 +550,9 @@ var ConfigStore = class {
428
550
 
429
551
  // ../core/src/tools/execution-context.ts
430
552
  async function createToolExecutionContext(profileName) {
431
- const profile = await new ConfigStore().showProfile(profileName);
432
- const credentials = await new CredentialStore().get(profile.name);
553
+ const store = new ConfigStore();
554
+ const profile = await store.showProfile(profileName);
555
+ const credentials = await store.getCredentials(profile.name);
433
556
  return { profile, credentials, api: new AiHubSpotApi(profile.openApiBaseUrl) };
434
557
  }
435
558
  function confirmationContext(context) {
@@ -485,7 +608,7 @@ var accountTools = [
485
608
  operation: "read",
486
609
  riskLevel: "low",
487
610
  inputSchema: { type: "object", additionalProperties: false },
488
- errorCodes: ["AI_HUB_CREDENTIAL_NOT_CONFIGURED", "AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE"],
611
+ errorCodes: ["AI_HUB_CREDENTIAL_NOT_CONFIGURED", "AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE", "AI_HUB_OPENAPI_BUSINESS_ERROR"],
489
612
  validate: (input) => strictObject(input, []),
490
613
  handler: async (_input, context) => {
491
614
  if (!context.credentials) throw new AiHubError("AI_HUB_CREDENTIAL_NOT_CONFIGURED", `Credentials are not configured for profile "${context.profile.name}".`);
@@ -496,7 +619,7 @@ var accountTools = [
496
619
 
497
620
  // ../core/src/tools/tool-utils.ts
498
621
  import { randomUUID as randomUUID2 } from "crypto";
499
- var signedReadErrors = ["AI_HUB_INVALID_ARGUMENT", "AI_HUB_CREDENTIAL_NOT_CONFIGURED", "AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE"];
622
+ var signedReadErrors = ["AI_HUB_INVALID_ARGUMENT", "AI_HUB_CREDENTIAL_NOT_CONFIGURED", "AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE", "AI_HUB_OPENAPI_BUSINESS_ERROR"];
500
623
  var writeErrors = [...signedReadErrors, "AI_HUB_WRITE_CONFIRMATION_REQUIRED", "AI_HUB_CONFIRMATION_REQUIRED", "AI_HUB_CONFIRMATION_EXPIRED", "AI_HUB_CONFIRMATION_CONTEXT_CHANGED", "AI_HUB_CONFIRMATION_NOT_FOUND"];
501
624
  function signed(context) {
502
625
  if (!context.credentials) throw new AiHubError("AI_HUB_CREDENTIAL_NOT_CONFIGURED", `Credentials are not configured for profile "${context.profile.name}".`);
@@ -744,7 +867,7 @@ var assetTools = [
744
867
  ];
745
868
 
746
869
  // ../core/src/tools/market-tools.ts
747
- var readErrors = ["AI_HUB_INVALID_ARGUMENT", "AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE"];
870
+ var readErrors = ["AI_HUB_INVALID_ARGUMENT", "AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE", "AI_HUB_OPENAPI_BUSINESS_ERROR"];
748
871
  var marketTools = [
749
872
  {
750
873
  name: "market_ping",
@@ -756,7 +879,7 @@ var marketTools = [
756
879
  operation: "read",
757
880
  riskLevel: "low",
758
881
  inputSchema: { type: "object", additionalProperties: false },
759
- errorCodes: ["AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE"],
882
+ errorCodes: ["AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE", "AI_HUB_OPENAPI_BUSINESS_ERROR"],
760
883
  validate: (input) => {
761
884
  strictObject(input, []);
762
885
  return {};
@@ -869,14 +992,186 @@ var marketTools = [
869
992
  }
870
993
  ];
871
994
 
995
+ // ../core/src/tools/symbol-rules.ts
996
+ var CACHE_TTL_MS = 5 * 60 * 1e3;
997
+ var cache = /* @__PURE__ */ new Map();
998
+ var pendingLoads = /* @__PURE__ */ new Map();
999
+ function cacheKey(context) {
1000
+ return `${context.profile.name}\0${context.profile.configVersion}\0${context.profile.openApiBaseUrl}`;
1001
+ }
1002
+ function normalizedSymbol(symbol) {
1003
+ return symbol.replaceAll("/", "").trim().toUpperCase();
1004
+ }
1005
+ function stringValue(value) {
1006
+ if (typeof value === "string" && value.trim()) return value.trim();
1007
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
1008
+ return void 0;
1009
+ }
1010
+ function nonNegativeInteger(value) {
1011
+ const result2 = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
1012
+ return Number.isInteger(result2) && result2 >= 0 ? result2 : void 0;
1013
+ }
1014
+ function rows(response) {
1015
+ if (Array.isArray(response)) return response.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
1016
+ if (!response || typeof response !== "object" || Array.isArray(response)) return [];
1017
+ const root = response;
1018
+ const candidates = [root.symbols, root.data, root.data?.symbols];
1019
+ for (const candidate of candidates) {
1020
+ if (Array.isArray(candidate)) return candidate.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
1021
+ }
1022
+ return [];
1023
+ }
1024
+ function parseRules(response) {
1025
+ const result2 = /* @__PURE__ */ new Map();
1026
+ for (const row of rows(response)) {
1027
+ const symbol = stringValue(row.symbol);
1028
+ if (!symbol) continue;
1029
+ result2.set(normalizedSymbol(symbol), {
1030
+ symbol,
1031
+ quantityPrecision: nonNegativeInteger(row.quantityPrecision),
1032
+ pricePrecision: nonNegativeInteger(row.pricePrecision),
1033
+ limitVolumeMin: stringValue(row.limitVolumeMin),
1034
+ limitAmountMin: stringValue(row.limitAmountMin),
1035
+ limitPriceMin: stringValue(row.limitPriceMin)
1036
+ });
1037
+ }
1038
+ return result2;
1039
+ }
1040
+ async function loadRules(context) {
1041
+ const key = cacheKey(context);
1042
+ const current = cache.get(key);
1043
+ if (current && current.expiresAt > Date.now()) return current;
1044
+ const pending = pendingLoads.get(key);
1045
+ if (pending) return pending;
1046
+ const load = context.api.symbols().then((response) => {
1047
+ const entry = { rules: parseRules(response), expiresAt: Date.now() + CACHE_TTL_MS };
1048
+ cache.set(key, entry);
1049
+ return entry;
1050
+ }).finally(() => pendingLoads.delete(key));
1051
+ pendingLoads.set(key, load);
1052
+ return load;
1053
+ }
1054
+ async function getSymbolRule(context, symbol) {
1055
+ const entry = await loadRules(context);
1056
+ const rule = entry.rules.get(normalizedSymbol(symbol));
1057
+ if (!rule) throw new AiHubError("AI_HUB_SYMBOL_NOT_FOUND", `Symbol "${symbol}" was not returned by the configured tenant OpenAPI symbols endpoint.`);
1058
+ return rule;
1059
+ }
1060
+ function decimalScale(value) {
1061
+ return value.split(".")[1]?.length ?? 0;
1062
+ }
1063
+ function decimalParts(value) {
1064
+ const [whole, fraction = ""] = value.split(".");
1065
+ return { digits: BigInt(`${whole}${fraction}`.replace(/^0+(?=\d)/, "") || "0"), scale: fraction.length };
1066
+ }
1067
+ function compareDecimal(left, right) {
1068
+ const a = decimalParts(left);
1069
+ const b = decimalParts(right);
1070
+ const scale = Math.max(a.scale, b.scale);
1071
+ const leftValue = a.digits * 10n ** BigInt(scale - a.scale);
1072
+ const rightValue = b.digits * 10n ** BigInt(scale - b.scale);
1073
+ return leftValue === rightValue ? 0 : leftValue > rightValue ? 1 : -1;
1074
+ }
1075
+ function multipliedDecimal(left, right) {
1076
+ const a = decimalParts(left);
1077
+ const b = decimalParts(right);
1078
+ const scale = a.scale + b.scale;
1079
+ const digits = (a.digits * b.digits).toString();
1080
+ if (scale === 0) return digits;
1081
+ const padded = digits.padStart(scale + 1, "0");
1082
+ return `${padded.slice(0, -scale)}.${padded.slice(-scale)}`;
1083
+ }
1084
+ function assertPrecision(name, value, precision, symbol) {
1085
+ if (precision !== void 0 && decimalScale(value) > precision) {
1086
+ throw new AiHubError("AI_HUB_SYMBOL_PRECISION_INVALID", `${name} for ${symbol} allows at most ${precision} decimal places; received ${value}.`);
1087
+ }
1088
+ }
1089
+ function assertAtLeast(name, value, minimum, symbol) {
1090
+ if (minimum && compareDecimal(value, minimum) < 0) {
1091
+ throw new AiHubError("AI_HUB_SYMBOL_MINIMUM_NOT_MET", `${name} for ${symbol} must be at least ${minimum}; received ${value}.`);
1092
+ }
1093
+ }
1094
+ async function preflightSymbolOrder(context, order) {
1095
+ const rule = await getSymbolRule(context, order.symbol);
1096
+ if (order.type === "MARKET" && order.side === "BUY" && order.quoteAmount) {
1097
+ assertPrecision("quoteAmount", order.quoteAmount, rule.pricePrecision, rule.symbol);
1098
+ }
1099
+ if (order.baseQuantity) assertPrecision("baseQuantity", order.baseQuantity, rule.quantityPrecision, rule.symbol);
1100
+ if (order.price) assertPrecision("price", order.price, rule.pricePrecision, rule.symbol);
1101
+ if (order.type === "LIMIT" && order.baseQuantity && order.price) {
1102
+ assertAtLeast("baseQuantity", order.baseQuantity, rule.limitVolumeMin, rule.symbol);
1103
+ assertAtLeast("price", order.price, rule.limitPriceMin, rule.symbol);
1104
+ if (rule.limitAmountMin && compareDecimal(rule.limitAmountMin, "0") > 0) {
1105
+ assertAtLeast("limit order amount", multipliedDecimal(order.baseQuantity, order.price), rule.limitAmountMin, rule.symbol);
1106
+ }
1107
+ }
1108
+ return rule;
1109
+ }
1110
+
872
1111
  // ../core/src/tools/margin-tools.ts
873
- function validateMarginOrder(input) {
874
- const value = strictObject(input, ["symbol", "volume", "side", "type", "price", "newClientOrderId", "isolated"]);
1112
+ function validateMarginSemanticOrder(input) {
1113
+ const value = strictObject(input, ["symbol", "side", "type", "quoteAmount", "baseQuantity", "price", "newClientOrderId", "isolated"]);
1114
+ const symbol = requiredString(value, "symbol");
1115
+ const side = requiredEnum(value, "side", ["BUY", "SELL"]);
875
1116
  const type = requiredEnum(value, "type", ["LIMIT", "MARKET"]);
876
- const price = optionalString(value, "price");
877
- if (type === "LIMIT" && !price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is required for LIMIT orders.");
878
- if (type === "MARKET" && price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is not allowed for MARKET orders.");
879
- return { symbol: requiredString(value, "symbol"), volume: positiveDecimal(value, "volume"), side: requiredEnum(value, "side", ["BUY", "SELL"]), type, ...price ? { price } : {}, newClientOrderId: optionalClientOrderId(value), ...optionalBoolean(value, "isolated") !== void 0 ? { isolated: optionalBoolean(value, "isolated") } : {} };
1117
+ const isolated = optionalBoolean(value, "isolated");
1118
+ const common = { symbol, side, type, newClientOrderId: optionalClientOrderId(value), ...isolated !== void 0 ? { isolated } : {} };
1119
+ if (type === "MARKET" && side === "BUY") {
1120
+ if (value.baseQuantity !== void 0) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "MARKET BUY uses quoteAmount (the amount of quote asset to spend), not baseQuantity.");
1121
+ if (value.price !== void 0) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is not allowed for MARKET orders.");
1122
+ const quoteAmount = positiveDecimal(value, "quoteAmount");
1123
+ return { ...common, volume: quoteAmount, intent: "market_buy", quoteAmount };
1124
+ }
1125
+ if (type === "MARKET" && side === "SELL") {
1126
+ if (value.quoteAmount !== void 0) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "MARKET SELL uses baseQuantity (the amount of base asset to sell), not quoteAmount.");
1127
+ if (value.price !== void 0) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is not allowed for MARKET orders.");
1128
+ const baseQuantity2 = positiveDecimal(value, "baseQuantity");
1129
+ return { ...common, volume: baseQuantity2, intent: "market_sell", baseQuantity: baseQuantity2 };
1130
+ }
1131
+ if (value.quoteAmount !== void 0) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "LIMIT orders use baseQuantity (the amount of base asset), not quoteAmount.");
1132
+ const baseQuantity = positiveDecimal(value, "baseQuantity");
1133
+ const price = positiveDecimal(value, "price");
1134
+ return { ...common, volume: baseQuantity, price, intent: "limit", baseQuantity };
1135
+ }
1136
+ function validateMarginMarketBuy(input) {
1137
+ const value = strictObject(input, ["symbol", "quoteAmount", "newClientOrderId", "isolated"]);
1138
+ return validateMarginSemanticOrder({ ...value, side: "BUY", type: "MARKET" });
1139
+ }
1140
+ function validateMarginMarketSell(input) {
1141
+ const value = strictObject(input, ["symbol", "baseQuantity", "newClientOrderId", "isolated"]);
1142
+ return validateMarginSemanticOrder({ ...value, side: "SELL", type: "MARKET" });
1143
+ }
1144
+ function validateMarginLimitOrder(input) {
1145
+ const value = strictObject(input, ["symbol", "side", "baseQuantity", "price", "newClientOrderId", "isolated"]);
1146
+ return validateMarginSemanticOrder({ ...value, type: "LIMIT" });
1147
+ }
1148
+ function toMarginOpenApiOrder(order) {
1149
+ return {
1150
+ symbol: order.symbol,
1151
+ volume: order.volume,
1152
+ side: order.side,
1153
+ type: order.type,
1154
+ newClientOrderId: order.newClientOrderId,
1155
+ ...order.price ? { price: order.price } : {},
1156
+ ...order.isolated !== void 0 ? { isolated: order.isolated } : {}
1157
+ };
1158
+ }
1159
+ function marginOrderSummary(order) {
1160
+ return {
1161
+ action: order.intent === "market_buy" ? "margin_market_buy" : order.intent === "market_sell" ? "margin_market_sell" : "margin_limit_order",
1162
+ symbol: order.symbol,
1163
+ side: order.side,
1164
+ type: order.type,
1165
+ ...order.quoteAmount ? { quoteAmount: order.quoteAmount, amountMeaning: "exact quote-asset amount to spend" } : {},
1166
+ ...order.baseQuantity ? { baseQuantity: order.baseQuantity, amountMeaning: "exact base-asset quantity" } : {},
1167
+ ...order.price ? { price: order.price } : {},
1168
+ isolated: order.isolated ?? false,
1169
+ newClientOrderId: order.newClientOrderId
1170
+ };
1171
+ }
1172
+ async function preflightMarginOrder(order, context) {
1173
+ await preflightSymbolOrder(context, order);
1174
+ return order;
880
1175
  }
881
1176
  function validateMarginOrderLookup(input) {
882
1177
  const value = strictObject(input, ["symbol", "orderId", "newClientOrderId", "isolated"]);
@@ -934,22 +1229,52 @@ var marginTools = [
934
1229
  handler: (input, context) => context.api.signedGet("/sapi/v2/margin/myTrades", input, signed(context))
935
1230
  },
936
1231
  {
937
- name: "margin_place_order",
938
- title: "Place Margin Order",
939
- description: "Create one v2 margin order after confirmation.",
940
- cliPath: ["margin", "order", "place"],
1232
+ name: "margin_market_buy",
1233
+ title: "Margin Market Buy by Quote Amount",
1234
+ description: "Spend an exact quote-asset amount for an isolated or cross-margin market buy. For ETHUSDT, quoteAmount is USDT. A market buy cannot guarantee an exact base-asset quantity.",
1235
+ cliPath: ["margin", "order", "market-buy"],
941
1236
  module: "spot-margin",
942
1237
  access: "signed",
943
1238
  operation: "write",
944
1239
  riskLevel: "high",
945
- inputSchema: { type: "object", properties: { symbol: { type: "string" }, volume: { type: "string" }, side: { type: "string", enum: ["BUY", "SELL"] }, type: { type: "string", enum: ["LIMIT", "MARKET"] }, price: { type: "string" }, newClientOrderId: { type: "string" }, isolated: { type: "boolean" } }, required: ["symbol", "volume", "side", "type"], additionalProperties: false },
1240
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, quoteAmount: { type: "string", description: "Required exact quote-asset amount to spend." }, newClientOrderId: { type: "string" }, isolated: { type: "boolean" } }, required: ["symbol", "quoteAmount"], additionalProperties: false },
946
1241
  errorCodes: writeErrors,
947
- validate: validateMarginOrder,
948
- handler: (input, context) => context.api.signedPost("/sapi/v2/margin/order", input, signed(context)),
949
- writeSummary: (input) => {
950
- const value = input;
951
- return { action: "margin_place_order", symbol: value.symbol, side: value.side, type: value.type, volume: value.volume, price: value.price ?? null, isolated: value.isolated ?? false, newClientOrderId: value.newClientOrderId };
952
- }
1242
+ validate: validateMarginMarketBuy,
1243
+ preflight: preflightMarginOrder,
1244
+ handler: (input, context) => context.api.signedPost("/sapi/v2/margin/order", toMarginOpenApiOrder(input), signed(context)),
1245
+ writeSummary: (input) => marginOrderSummary(input)
1246
+ },
1247
+ {
1248
+ name: "margin_market_sell",
1249
+ title: "Margin Market Sell by Base Quantity",
1250
+ description: "Sell an exact base-asset quantity at market for an isolated or cross-margin account. For ETHUSDT, baseQuantity is ETH.",
1251
+ cliPath: ["margin", "order", "market-sell"],
1252
+ module: "spot-margin",
1253
+ access: "signed",
1254
+ operation: "write",
1255
+ riskLevel: "high",
1256
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, baseQuantity: { type: "string", description: "Required exact base-asset quantity to sell." }, newClientOrderId: { type: "string" }, isolated: { type: "boolean" } }, required: ["symbol", "baseQuantity"], additionalProperties: false },
1257
+ errorCodes: writeErrors,
1258
+ validate: validateMarginMarketSell,
1259
+ preflight: preflightMarginOrder,
1260
+ handler: (input, context) => context.api.signedPost("/sapi/v2/margin/order", toMarginOpenApiOrder(input), signed(context)),
1261
+ writeSummary: (input) => marginOrderSummary(input)
1262
+ },
1263
+ {
1264
+ name: "margin_limit_order",
1265
+ title: "Place Margin Limit Order",
1266
+ description: "Place an isolated or cross-margin LIMIT BUY or SELL using an exact base-asset quantity and a limit price.",
1267
+ cliPath: ["margin", "order", "limit"],
1268
+ module: "spot-margin",
1269
+ access: "signed",
1270
+ operation: "write",
1271
+ riskLevel: "high",
1272
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, side: { type: "string", enum: ["BUY", "SELL"] }, baseQuantity: { type: "string", description: "Required exact base-asset quantity." }, price: { type: "string", description: "Required limit price in the quote asset." }, newClientOrderId: { type: "string" }, isolated: { type: "boolean" } }, required: ["symbol", "side", "baseQuantity", "price"], additionalProperties: false },
1273
+ errorCodes: writeErrors,
1274
+ validate: validateMarginLimitOrder,
1275
+ preflight: preflightMarginOrder,
1276
+ handler: (input, context) => context.api.signedPost("/sapi/v2/margin/order", toMarginOpenApiOrder(input), signed(context)),
1277
+ writeSummary: (input) => marginOrderSummary(input)
953
1278
  },
954
1279
  {
955
1280
  name: "margin_cancel_order",
@@ -973,7 +1298,7 @@ var marginTools = [
973
1298
 
974
1299
  // ../core/src/tools/order-tools.ts
975
1300
  import { randomUUID as randomUUID3 } from "crypto";
976
- var signedReadErrors2 = ["AI_HUB_INVALID_ARGUMENT", "AI_HUB_CREDENTIAL_NOT_CONFIGURED", "AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE"];
1301
+ var signedReadErrors2 = ["AI_HUB_INVALID_ARGUMENT", "AI_HUB_CREDENTIAL_NOT_CONFIGURED", "AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE", "AI_HUB_OPENAPI_BUSINESS_ERROR"];
977
1302
  var writeErrors2 = [...signedReadErrors2, "AI_HUB_WRITE_CONFIRMATION_REQUIRED", "AI_HUB_CONFIRMATION_REQUIRED", "AI_HUB_CONFIRMATION_EXPIRED", "AI_HUB_CONFIRMATION_CONTEXT_CHANGED", "AI_HUB_CONFIRMATION_NOT_FOUND"];
978
1303
  var decimal = /^(?:0|[1-9]\d*)(?:\.\d+)?$/;
979
1304
  function positiveDecimal2(value, name) {
@@ -1002,53 +1327,89 @@ function clientOrderId(value) {
1002
1327
  if (supplied) return supplied;
1003
1328
  return `agent_${randomUUID3().replaceAll("-", "").slice(0, 24)}`;
1004
1329
  }
1005
- function validatePlaceOrder(input) {
1006
- const value = strictObject(input, ["symbol", "volume", "side", "type", "price", "timeInForce", "newClientOrderId", "recvWindow"]);
1007
- const type = orderType(value);
1008
- const price = optionalString(value, "price");
1009
- if (type === "LIMIT" && !price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is required for LIMIT orders.");
1010
- if (type === "MARKET" && price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is not allowed for MARKET orders.");
1330
+ function optionalOrderFields(value) {
1011
1331
  const timeInForce = optionalString(value, "timeInForce")?.toUpperCase();
1012
1332
  if (timeInForce && !["GTC", "IOC", "FOK"].includes(timeInForce)) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "timeInForce must be GTC, IOC, or FOK.");
1333
+ const recvWindow = optionalString(value, "recvWindow");
1013
1334
  return {
1014
- symbol: requiredString(value, "symbol"),
1015
- volume: positiveDecimal2(value, "volume"),
1016
- side: orderSide(value),
1017
- type,
1018
- ...price ? { price } : {},
1019
- ...timeInForce ? { timeInForce } : {},
1020
1335
  newClientOrderId: clientOrderId(value),
1021
- ...optionalString(value, "recvWindow") ? { recvWindow: optionalString(value, "recvWindow") } : {}
1336
+ ...recvWindow ? { recvWindow } : {},
1337
+ ...timeInForce ? { timeInForce } : {}
1338
+ };
1339
+ }
1340
+ function validateSemanticOrder(input) {
1341
+ const value = strictObject(input, ["symbol", "side", "type", "quoteAmount", "baseQuantity", "price", "timeInForce", "newClientOrderId", "recvWindow"]);
1342
+ const side = orderSide(value);
1343
+ const type = orderType(value);
1344
+ const symbol = requiredString(value, "symbol");
1345
+ if (type === "MARKET" && side === "BUY") {
1346
+ if (value.baseQuantity !== void 0) {
1347
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "MARKET BUY uses quoteAmount (the amount of quote asset to spend), not baseQuantity.");
1348
+ }
1349
+ if (value.price !== void 0 || value.timeInForce !== void 0) {
1350
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price and timeInForce are not allowed for MARKET orders.");
1351
+ }
1352
+ const quoteAmount = positiveDecimal2(value, "quoteAmount");
1353
+ return { symbol, volume: quoteAmount, side, type, intent: "market_buy", quoteAmount, ...optionalOrderFields(value) };
1354
+ }
1355
+ if (type === "MARKET" && side === "SELL") {
1356
+ if (value.quoteAmount !== void 0) {
1357
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "MARKET SELL uses baseQuantity (the amount of base asset to sell), not quoteAmount.");
1358
+ }
1359
+ if (value.price !== void 0 || value.timeInForce !== void 0) {
1360
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price and timeInForce are not allowed for MARKET orders.");
1361
+ }
1362
+ const baseQuantity2 = positiveDecimal2(value, "baseQuantity");
1363
+ return { symbol, volume: baseQuantity2, side, type, intent: "market_sell", baseQuantity: baseQuantity2, ...optionalOrderFields(value) };
1364
+ }
1365
+ if (value.quoteAmount !== void 0) {
1366
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "LIMIT orders use baseQuantity (the amount of base asset), not quoteAmount.");
1367
+ }
1368
+ const baseQuantity = positiveDecimal2(value, "baseQuantity");
1369
+ const price = positiveDecimal2(value, "price");
1370
+ return { symbol, volume: baseQuantity, side, type, price, intent: "limit", baseQuantity, ...optionalOrderFields(value) };
1371
+ }
1372
+ function validateMarketBuy(input) {
1373
+ const value = strictObject(input, ["symbol", "quoteAmount", "newClientOrderId", "recvWindow"]);
1374
+ return validateSemanticOrder({ ...value, side: "BUY", type: "MARKET" });
1375
+ }
1376
+ function toOpenApiOrder(order) {
1377
+ return {
1378
+ symbol: order.symbol,
1379
+ volume: order.volume,
1380
+ side: order.side,
1381
+ type: order.type,
1382
+ newClientOrderId: order.newClientOrderId,
1383
+ ...order.price ? { price: order.price } : {},
1384
+ ...order.timeInForce ? { timeInForce: order.timeInForce } : {},
1385
+ ...order.recvWindow ? { recvWindow: order.recvWindow } : {}
1022
1386
  };
1023
1387
  }
1388
+ function validateMarketSell(input) {
1389
+ const value = strictObject(input, ["symbol", "baseQuantity", "newClientOrderId", "recvWindow"]);
1390
+ return validateSemanticOrder({ ...value, side: "SELL", type: "MARKET" });
1391
+ }
1392
+ function validateLimitOrder(input) {
1393
+ const value = strictObject(input, ["symbol", "side", "baseQuantity", "price", "timeInForce", "newClientOrderId", "recvWindow"]);
1394
+ return validateSemanticOrder({ ...value, type: "LIMIT" });
1395
+ }
1024
1396
  function validateCancelOrder(input) {
1025
1397
  const value = strictObject(input, ["symbol", "orderId", "newClientOrderId"]);
1026
1398
  return { symbol: requiredString(value, "symbol"), orderId: requiredString(value, "orderId"), ...optionalString(value, "newClientOrderId") ? { newClientOrderId: optionalString(value, "newClientOrderId") } : {} };
1027
1399
  }
1028
1400
  function validateTestOrder(input) {
1029
- const value = strictObject(input, ["symbol", "volume", "side", "type", "price", "newClientOrderId", "recvWindow"]);
1030
- const type = orderType(value);
1031
- const price = optionalString(value, "price");
1032
- if (type === "LIMIT" && !price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is required for LIMIT orders.");
1033
- if (type === "MARKET" && price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "price is not allowed for MARKET orders.");
1034
- return { symbol: requiredString(value, "symbol"), volume: positiveDecimal2(value, "volume"), side: orderSide(value), type, ...price ? { price } : {}, ...optionalString(value, "newClientOrderId") ? { newClientOrderId: optionalString(value, "newClientOrderId") } : {}, ...optionalString(value, "recvWindow") ? { recvWindow: optionalString(value, "recvWindow") } : {} };
1401
+ return toOpenApiOrder(validateSemanticOrder(input));
1035
1402
  }
1036
1403
  function validateBatchOrders(input) {
1037
1404
  const value = strictObject(input, ["symbol", "orders"]);
1405
+ const symbol = requiredString(value, "symbol");
1038
1406
  const orders = value.orders;
1039
1407
  if (!Array.isArray(orders) || orders.length < 1 || orders.length > 10) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", "orders must contain between 1 and 10 orders.");
1040
- const normalized = orders.map((item, index) => {
1041
- if (!item || typeof item !== "object" || Array.isArray(item)) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `orders[${index}] must be an object.`);
1042
- const order = item;
1043
- const price = optionalString(order, "price");
1044
- const volume = positiveDecimal2(order, "volume");
1045
- const side = orderSide(order);
1046
- const batchType = requiredEnum(order, "batchType", ["LIMIT", "MARKET"]);
1047
- if (batchType === "LIMIT" && !price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `orders[${index}].price is required for LIMIT.`);
1048
- if (batchType === "MARKET" && price) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `orders[${index}].price is not allowed for MARKET.`);
1049
- return { volume, side, batchType, ...price ? { price } : {} };
1050
- });
1051
- return { symbol: requiredString(value, "symbol"), orders: normalized };
1408
+ return { symbol, orders: orders.map((order, index) => {
1409
+ if (!order || typeof order !== "object" || Array.isArray(order)) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `orders[${index}] must be an object.`);
1410
+ const semantic = validateSemanticOrder({ ...order, symbol });
1411
+ return { volume: semantic.volume, side: semantic.side, batchType: semantic.type, ...semantic.price ? { price: semantic.price } : {} };
1412
+ }) };
1052
1413
  }
1053
1414
  function validateBatchCancel(input) {
1054
1415
  const value = strictObject(input, ["symbol", "orderIds"]);
@@ -1057,17 +1418,44 @@ function validateBatchCancel(input) {
1057
1418
  }
1058
1419
  return { symbol: requiredString(value, "symbol"), orderIds: value.orderIds.map((id) => id.trim()) };
1059
1420
  }
1421
+ function semanticOrderSummary(order) {
1422
+ return {
1423
+ action: order.intent === "market_buy" ? "market_buy" : order.intent === "market_sell" ? "market_sell" : "limit_order",
1424
+ symbol: order.symbol,
1425
+ side: order.side,
1426
+ type: order.type,
1427
+ ...order.quoteAmount ? { quoteAmount: order.quoteAmount, amountMeaning: "exact quote-asset amount to spend" } : {},
1428
+ ...order.baseQuantity ? { baseQuantity: order.baseQuantity, amountMeaning: "exact base-asset quantity" } : {},
1429
+ ...order.price ? { price: order.price } : {},
1430
+ ...order.timeInForce ? { timeInForce: order.timeInForce } : {},
1431
+ newClientOrderId: order.newClientOrderId
1432
+ };
1433
+ }
1434
+ async function preflightSpotOrder(order, context) {
1435
+ await preflightSymbolOrder(context, order);
1436
+ return order;
1437
+ }
1438
+ async function preflightSpotBatch(input, context) {
1439
+ await Promise.all(input.orders.map((order) => preflightSymbolOrder(context, {
1440
+ symbol: input.symbol,
1441
+ side: order.side,
1442
+ type: order.batchType,
1443
+ ...order.batchType === "MARKET" && order.side === "BUY" ? { quoteAmount: order.volume } : { baseQuantity: order.volume },
1444
+ ...order.price ? { price: order.price } : {}
1445
+ })));
1446
+ return input;
1447
+ }
1060
1448
  var orderTools = [
1061
1449
  {
1062
1450
  name: "spot_test_order",
1063
1451
  title: "Test Spot Order",
1064
- description: "Validate one LIMIT or MARKET spot order without sending it to the matching engine.",
1452
+ description: "Validate an order without sending it to the matching engine. MARKET BUY requires quoteAmount (quote asset to spend); MARKET SELL and LIMIT require baseQuantity. LIMIT also requires price.",
1065
1453
  cliPath: ["spot", "order", "test"],
1066
1454
  module: "spot-order",
1067
1455
  access: "signed",
1068
1456
  operation: "read",
1069
1457
  riskLevel: "low",
1070
- inputSchema: { type: "object", properties: { symbol: { type: "string" }, volume: { type: "string" }, side: { type: "string", enum: ["BUY", "SELL"] }, type: { type: "string", enum: ["LIMIT", "MARKET"] }, price: { type: "string" }, newClientOrderId: { type: "string" }, recvWindow: { type: "string" } }, required: ["symbol", "volume", "side", "type"], additionalProperties: false },
1458
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, side: { type: "string", enum: ["BUY", "SELL"] }, type: { type: "string", enum: ["LIMIT", "MARKET"] }, quoteAmount: { type: "string", description: "Required only for MARKET BUY: exact quote-asset amount to spend." }, baseQuantity: { type: "string", description: "Required for MARKET SELL and LIMIT: exact base-asset quantity." }, price: { type: "string", description: "Required only for LIMIT." }, timeInForce: { type: "string", enum: ["GTC", "IOC", "FOK"] }, newClientOrderId: { type: "string" }, recvWindow: { type: "string" } }, required: ["symbol", "side", "type"], additionalProperties: false },
1071
1459
  errorCodes: signedReadErrors2,
1072
1460
  validate: validateTestOrder,
1073
1461
  handler: (input, context) => context.api.signedPost("/sapi/v2/order/test", input, signed2(context))
@@ -1092,15 +1480,16 @@ var orderTools = [
1092
1480
  {
1093
1481
  name: "spot_batch_place_orders",
1094
1482
  title: "Batch Place Spot Orders",
1095
- description: "Create up to 10 spot orders after confirmation.",
1483
+ description: "Create up to 10 spot orders after confirmation. Each MARKET BUY item requires quoteAmount; MARKET SELL and LIMIT items require baseQuantity; LIMIT items also require price.",
1096
1484
  cliPath: ["spot", "order", "batch-place"],
1097
1485
  module: "spot-order",
1098
1486
  access: "signed",
1099
1487
  operation: "write",
1100
1488
  riskLevel: "high",
1101
- inputSchema: { type: "object", properties: { symbol: { type: "string" }, orders: { type: "array" } }, required: ["symbol", "orders"], additionalProperties: false },
1489
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, orders: { type: "array", description: "Items use side/type plus quoteAmount for MARKET BUY, baseQuantity for MARKET SELL/LIMIT, and price for LIMIT." } }, required: ["symbol", "orders"], additionalProperties: false },
1102
1490
  errorCodes: writeErrors2,
1103
1491
  validate: validateBatchOrders,
1492
+ preflight: preflightSpotBatch,
1104
1493
  handler: (input, context) => context.api.signedPost("/sapi/v2/batchOrders", input, signed2(context)),
1105
1494
  writeSummary: (input) => {
1106
1495
  const value = input;
@@ -1127,7 +1516,7 @@ var orderTools = [
1127
1516
  },
1128
1517
  {
1129
1518
  name: "spot_get_open_orders",
1130
- title: "Get Open Spot Orders",
1519
+ title: "Get Spot Open Orders",
1131
1520
  description: "Get current signed spot orders.",
1132
1521
  cliPath: ["spot", "order", "open"],
1133
1522
  module: "spot-order",
@@ -1160,22 +1549,52 @@ var orderTools = [
1160
1549
  handler: (input, context) => context.api.getMyTrades(input, signed2(context))
1161
1550
  },
1162
1551
  {
1163
- name: "spot_place_order",
1164
- title: "Place Spot Order",
1165
- description: "Create one LIMIT or MARKET spot order after confirmation.",
1166
- cliPath: ["spot", "order", "place"],
1552
+ name: "spot_market_buy",
1553
+ title: "Market Buy by Quote Amount",
1554
+ description: "Spend an exact quote-asset amount to buy at market. For ETHUSDT, quoteAmount is USDT. Do not use this tool to request a base-asset quantity such as 1 ETH; market execution cannot guarantee an exact base quantity.",
1555
+ cliPath: ["spot", "order", "market-buy"],
1167
1556
  module: "spot-order",
1168
1557
  access: "signed",
1169
1558
  operation: "write",
1170
1559
  riskLevel: "high",
1171
- inputSchema: { type: "object", properties: { symbol: { type: "string" }, volume: { type: "string" }, side: { type: "string", enum: ["BUY", "SELL"] }, type: { type: "string", enum: ["LIMIT", "MARKET"] }, price: { type: "string" }, timeInForce: { type: "string", enum: ["GTC", "IOC", "FOK"] }, newClientOrderId: { type: "string" }, recvWindow: { type: "string" } }, required: ["symbol", "volume", "side", "type"], additionalProperties: false },
1560
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, quoteAmount: { type: "string", description: "Required exact quote-asset amount to spend, e.g. 100 means spend 100 USDT for ETHUSDT." }, newClientOrderId: { type: "string" }, recvWindow: { type: "string" } }, required: ["symbol", "quoteAmount"], additionalProperties: false },
1172
1561
  errorCodes: writeErrors2,
1173
- validate: validatePlaceOrder,
1174
- handler: (input, context) => context.api.placeOrder(input, signed2(context)),
1175
- writeSummary: (input) => {
1176
- const value = input;
1177
- return { action: "place_order", symbol: value.symbol, side: value.side, type: value.type, volume: value.volume, price: value.price ?? null, timeInForce: value.timeInForce ?? null, newClientOrderId: value.newClientOrderId };
1178
- }
1562
+ validate: validateMarketBuy,
1563
+ preflight: preflightSpotOrder,
1564
+ handler: (input, context) => context.api.placeOrder(toOpenApiOrder(input), signed2(context)),
1565
+ writeSummary: (input) => semanticOrderSummary(input)
1566
+ },
1567
+ {
1568
+ name: "spot_market_sell",
1569
+ title: "Market Sell by Base Quantity",
1570
+ description: "Sell an exact base-asset quantity at market. For ETHUSDT, baseQuantity is ETH.",
1571
+ cliPath: ["spot", "order", "market-sell"],
1572
+ module: "spot-order",
1573
+ access: "signed",
1574
+ operation: "write",
1575
+ riskLevel: "high",
1576
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, baseQuantity: { type: "string", description: "Required exact base-asset quantity to sell, e.g. 0.5 means sell 0.5 ETH for ETHUSDT." }, newClientOrderId: { type: "string" }, recvWindow: { type: "string" } }, required: ["symbol", "baseQuantity"], additionalProperties: false },
1577
+ errorCodes: writeErrors2,
1578
+ validate: validateMarketSell,
1579
+ preflight: preflightSpotOrder,
1580
+ handler: (input, context) => context.api.placeOrder(toOpenApiOrder(input), signed2(context)),
1581
+ writeSummary: (input) => semanticOrderSummary(input)
1582
+ },
1583
+ {
1584
+ name: "spot_limit_order",
1585
+ title: "Place Limit Spot Order",
1586
+ description: "Place a LIMIT BUY or SELL using an exact base-asset quantity and a limit price.",
1587
+ cliPath: ["spot", "order", "limit"],
1588
+ module: "spot-order",
1589
+ access: "signed",
1590
+ operation: "write",
1591
+ riskLevel: "high",
1592
+ inputSchema: { type: "object", properties: { symbol: { type: "string" }, side: { type: "string", enum: ["BUY", "SELL"] }, baseQuantity: { type: "string", description: "Required exact base-asset quantity." }, price: { type: "string", description: "Required limit price in the quote asset." }, timeInForce: { type: "string", enum: ["GTC", "IOC", "FOK"] }, newClientOrderId: { type: "string" }, recvWindow: { type: "string" } }, required: ["symbol", "side", "baseQuantity", "price"], additionalProperties: false },
1593
+ errorCodes: writeErrors2,
1594
+ validate: validateLimitOrder,
1595
+ preflight: preflightSpotOrder,
1596
+ handler: (input, context) => context.api.placeOrder(toOpenApiOrder(input), signed2(context)),
1597
+ writeSummary: (input) => semanticOrderSummary(input)
1179
1598
  },
1180
1599
  {
1181
1600
  name: "spot_cancel_order",
@@ -1446,12 +1865,12 @@ var ToolRegistry = class {
1446
1865
  toolsByCliPath = /* @__PURE__ */ new Map();
1447
1866
  constructor(tools = allTools) {
1448
1867
  for (const tool of tools) {
1449
- const path = tool.cliPath.join(" ");
1450
- if (this.toolsByName.has(tool.name) || this.toolsByCliPath.has(path)) {
1868
+ const path2 = tool.cliPath.join(" ");
1869
+ if (this.toolsByName.has(tool.name) || this.toolsByCliPath.has(path2)) {
1451
1870
  throw new AiHubError("AI_HUB_TOOL_DUPLICATE", `Duplicate tool registration: ${tool.name}.`);
1452
1871
  }
1453
1872
  this.toolsByName.set(tool.name, tool);
1454
- this.toolsByCliPath.set(path, tool);
1873
+ this.toolsByCliPath.set(path2, tool);
1455
1874
  }
1456
1875
  }
1457
1876
  list(options = {}) {
@@ -1464,10 +1883,10 @@ var ToolRegistry = class {
1464
1883
  }
1465
1884
  return tool;
1466
1885
  }
1467
- byCliPath(path, options = {}) {
1468
- const tool = this.toolsByCliPath.get(path.join(" "));
1886
+ byCliPath(path2, options = {}) {
1887
+ const tool = this.toolsByCliPath.get(path2.join(" "));
1469
1888
  if (!tool || options.readOnly && tool.operation !== "read") {
1470
- throw new AiHubError("AI_HUB_TOOL_NOT_AVAILABLE", `Command "${path.join(" ")}" is not available.`);
1889
+ throw new AiHubError("AI_HUB_TOOL_NOT_AVAILABLE", `Command "${path2.join(" ")}" is not available.`);
1471
1890
  }
1472
1891
  return tool;
1473
1892
  }
@@ -1477,22 +1896,22 @@ var ToolRegistry = class {
1477
1896
  const validInput = tool.validate(input);
1478
1897
  return tool.handler(validInput, context);
1479
1898
  }
1480
- prepareWrite(name, input, context, confirmations) {
1899
+ async prepareWrite(name, input, context, confirmations) {
1481
1900
  const tool = this.byName(name);
1482
1901
  if (tool.operation !== "write" || !tool.writeSummary) throw new AiHubError("AI_HUB_TOOL_NOT_WRITE", `Tool "${name}" is not a confirmable write Tool.`);
1483
1902
  const validInput = tool.validate(input);
1903
+ const preflightInput = tool.preflight ? await tool.preflight(validInput, context) : validInput;
1484
1904
  return confirmations.prepare({
1485
1905
  action: tool.name,
1486
- payload: validInput,
1906
+ payload: preflightInput,
1487
1907
  context: confirmationContext(context),
1488
- summary: tool.writeSummary(validInput)
1908
+ summary: tool.writeSummary(preflightInput)
1489
1909
  });
1490
1910
  }
1491
1911
  async executeConfirmed(action, payload, context) {
1492
1912
  const tool = this.byName(action);
1493
1913
  if (tool.operation !== "write") throw new AiHubError("AI_HUB_TOOL_NOT_WRITE", `Tool "${action}" is not a write Tool.`);
1494
- const validInput = tool.validate(payload);
1495
- return tool.handler(validInput, context);
1914
+ return tool.handler(payload, context);
1496
1915
  }
1497
1916
  capabilities(context, options = {}) {
1498
1917
  return this.list(options).map((tool) => ({
@@ -1532,6 +1951,176 @@ var ToolWriteExecutor = class {
1532
1951
  }
1533
1952
  };
1534
1953
 
1954
+ // ../core/src/setup.ts
1955
+ import { execFileSync } from "child_process";
1956
+ import * as fs from "fs";
1957
+ import * as os from "os";
1958
+ import * as path from "path";
1959
+ var MCP_BINARY = "ai-hub-trade-mcp";
1960
+ var MCP_CLIENT_NAMES = {
1961
+ cursor: "Cursor",
1962
+ "claude-desktop": "Claude Desktop",
1963
+ "claude-code": "Claude Code",
1964
+ codex: "Codex"
1965
+ };
1966
+ var SUPPORTED_MCP_CLIENTS = Object.keys(MCP_CLIENT_NAMES);
1967
+ function runtime() {
1968
+ const home = os.homedir();
1969
+ return {
1970
+ home,
1971
+ platform: process.platform,
1972
+ cwd: process.cwd(),
1973
+ appData: process.env.APPDATA,
1974
+ localAppData: process.env.LOCALAPPDATA,
1975
+ xdgConfigHome: process.env.XDG_CONFIG_HOME
1976
+ };
1977
+ }
1978
+ function windowsAppData(value, home) {
1979
+ return value ?? path.join(home, "AppData", "Roaming");
1980
+ }
1981
+ function findMicrosoftStoreClaudePath(value, home) {
1982
+ const packagesDirectory = path.join(value ?? path.join(home, "AppData", "Local"), "Packages");
1983
+ try {
1984
+ const packageName = fs.readdirSync(packagesDirectory).find((entry) => entry.startsWith("Claude_"));
1985
+ if (!packageName) return void 0;
1986
+ const configPath = path.join(
1987
+ packagesDirectory,
1988
+ packageName,
1989
+ "LocalCache",
1990
+ "Roaming",
1991
+ "Claude",
1992
+ "claude_desktop_config.json"
1993
+ );
1994
+ return fs.existsSync(configPath) || fs.existsSync(path.dirname(configPath)) ? configPath : void 0;
1995
+ } catch {
1996
+ return void 0;
1997
+ }
1998
+ }
1999
+ function getMcpClientConfigPath(client, value = runtime()) {
2000
+ if (client === "cursor") return path.join(value.home, ".cursor", "mcp.json");
2001
+ if (value.platform === "win32") {
2002
+ return findMicrosoftStoreClaudePath(value.localAppData, value.home) ?? path.join(windowsAppData(value.appData, value.home), "Claude", "claude_desktop_config.json");
2003
+ }
2004
+ if (value.platform === "darwin") {
2005
+ return path.join(value.home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
2006
+ }
2007
+ return path.join(value.xdgConfigHome ?? path.join(value.home, ".config"), "Claude", "claude_desktop_config.json");
2008
+ }
2009
+ function buildServerSpec(profile, launch) {
2010
+ return {
2011
+ name: serverName(profile),
2012
+ command: launch.command,
2013
+ args: [...launch.args, ...profile ? ["--profile", profile] : []]
2014
+ };
2015
+ }
2016
+ function serverName(profile) {
2017
+ return profile ? `${MCP_BINARY}-${profile}` : MCP_BINARY;
2018
+ }
2019
+ function isRecord(value) {
2020
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2021
+ }
2022
+ function mergeJsonMcpConfig(configPath, server) {
2023
+ const directory = path.dirname(configPath);
2024
+ fs.mkdirSync(directory, { recursive: true });
2025
+ const configExists = fs.existsSync(configPath);
2026
+ let config = {};
2027
+ if (configExists) {
2028
+ const raw = fs.readFileSync(configPath, "utf8");
2029
+ try {
2030
+ const parsed = JSON.parse(raw);
2031
+ if (!isRecord(parsed)) throw new Error("root must be a JSON object");
2032
+ config = parsed;
2033
+ } catch {
2034
+ throw new Error(`Failed to parse existing MCP configuration at ${configPath}. No changes were made.`);
2035
+ }
2036
+ }
2037
+ if (config.mcpServers === void 0) {
2038
+ config.mcpServers = {};
2039
+ }
2040
+ if (!isRecord(config.mcpServers)) {
2041
+ throw new Error(`Existing MCP configuration at ${configPath} has an invalid mcpServers object. No changes were made.`);
2042
+ }
2043
+ if (configExists) {
2044
+ const backupPath = `${configPath}.bak`;
2045
+ if (!fs.existsSync(backupPath)) {
2046
+ fs.copyFileSync(configPath, backupPath);
2047
+ process.stdout.write(`Backup created: ${backupPath}
2048
+ `);
2049
+ }
2050
+ }
2051
+ config.mcpServers[server.name] = { command: server.command, args: server.args };
2052
+ const mode = configExists ? fs.statSync(configPath).mode & 511 : 384;
2053
+ const temporaryPath = `${configPath}.${process.pid}.tmp`;
2054
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(config, null, 2)}
2055
+ `, { encoding: "utf8", mode });
2056
+ fs.chmodSync(temporaryPath, mode);
2057
+ fs.renameSync(temporaryPath, configPath);
2058
+ }
2059
+ function executeClientRegistration(command, args, value) {
2060
+ process.stdout.write(`Running: ${command} ${args.join(" ")}
2061
+ `);
2062
+ if (value.executeClientCommand) {
2063
+ value.executeClientCommand(command, args);
2064
+ return;
2065
+ }
2066
+ execFileSync(command, args, { stdio: "inherit" });
2067
+ }
2068
+ function jsonFileAdapter(id) {
2069
+ return {
2070
+ id,
2071
+ name: MCP_CLIENT_NAMES[id],
2072
+ install(server, value) {
2073
+ mergeJsonMcpConfig(getMcpClientConfigPath(id, value), server);
2074
+ process.stdout.write(`Configured ${this.name}: ${getMcpClientConfigPath(id, value)}
2075
+ Restart ${this.name} to apply changes.
2076
+ `);
2077
+ }
2078
+ };
2079
+ }
2080
+ function cliAdapter(id) {
2081
+ const command = id === "claude-code" ? "claude" : "codex";
2082
+ return {
2083
+ id,
2084
+ name: MCP_CLIENT_NAMES[id],
2085
+ install(server, value) {
2086
+ const args = [
2087
+ "mcp",
2088
+ "add",
2089
+ ...id === "claude-code" ? ["--scope", "user", "--transport", "stdio"] : [],
2090
+ server.name,
2091
+ "--",
2092
+ server.command,
2093
+ ...server.args
2094
+ ];
2095
+ executeClientRegistration(command, args, value);
2096
+ process.stdout.write(`Configured ${this.name} with local stdio MCP server "${server.name}".
2097
+ `);
2098
+ }
2099
+ };
2100
+ }
2101
+ var MCP_CLIENT_ADAPTERS = {
2102
+ cursor: jsonFileAdapter("cursor"),
2103
+ "claude-desktop": jsonFileAdapter("claude-desktop"),
2104
+ "claude-code": cliAdapter("claude-code"),
2105
+ codex: cliAdapter("codex")
2106
+ };
2107
+ function runMcpSetup(options, value = runtime()) {
2108
+ const adapter = MCP_CLIENT_ADAPTERS[options.client];
2109
+ if (!adapter) {
2110
+ throw new Error(`Unknown MCP client "${options.client}". Supported clients: ${SUPPORTED_MCP_CLIENTS.join(", ")}.`);
2111
+ }
2112
+ const profile = options.profile ? validateProfileName(options.profile) : void 0;
2113
+ adapter.install(buildServerSpec(profile, options.launch), value);
2114
+ }
2115
+ function printMcpSetupUsage() {
2116
+ process.stdout.write(
2117
+ `Usage: ${MCP_BINARY} setup --client <client> [--profile <name>]
2118
+
2119
+ Supported clients:
2120
+ ` + SUPPORTED_MCP_CLIENTS.map((client) => ` ${client.padEnd(16)} ${MCP_CLIENT_NAMES[client]}`).join("\n") + "\n"
2121
+ );
2122
+ }
2123
+
1535
2124
  // src/server.ts
1536
2125
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1537
2126
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -1540,8 +2129,8 @@ var CONFIRM_ACTION_TOOL = "confirm_action";
1540
2129
  function result(data) {
1541
2130
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1542
2131
  }
1543
- function errorResult(error) {
1544
- const payload = error instanceof AiHubError ? { code: error.code, message: error.message } : { code: "AI_HUB_UNEXPECTED_ERROR", message: error instanceof Error ? error.message : "Unexpected error" };
2132
+ function toMcpErrorResult(error) {
2133
+ const payload = toAiHubErrorPayload(error);
1545
2134
  return { isError: true, content: [{ type: "text", text: JSON.stringify({ ok: false, ...payload }) }] };
1546
2135
  }
1547
2136
  function toMcpTool(tool) {
@@ -1575,10 +2164,10 @@ function createServer(profileName, readOnly) {
1575
2164
  const registry = createToolRegistry();
1576
2165
  const writeExecutor = new ToolWriteExecutor(registry);
1577
2166
  const server = new Server(
1578
- { name: "ai-hub-agent-trade", version: "0.1.0" },
2167
+ { name: "ai-hub-agent-trade", version: "0.1.3" },
1579
2168
  {
1580
2169
  capabilities: { tools: {} },
1581
- instructions: "For every state-changing action, call only a spot_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."
2170
+ instructions: "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."
1582
2171
  }
1583
2172
  );
1584
2173
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -1622,12 +2211,12 @@ function createServer(profileName, readOnly) {
1622
2211
  }
1623
2212
  const preparedTool = registry.list({ readOnly: false }).find((tool) => tool.operation === "write" && prepareToolName(tool) === request.params.name);
1624
2213
  if (preparedTool) {
1625
- return result({ ok: true, data: writeExecutor.prepare(preparedTool.name, request.params.arguments ?? {}, context) });
2214
+ return result({ ok: true, data: await writeExecutor.prepare(preparedTool.name, request.params.arguments ?? {}, context) });
1626
2215
  }
1627
2216
  const data = await registry.execute(request.params.name, request.params.arguments ?? {}, context, { readOnly });
1628
2217
  return result({ ok: true, data });
1629
2218
  } catch (error) {
1630
- return errorResult(error);
2219
+ return toMcpErrorResult(error);
1631
2220
  }
1632
2221
  });
1633
2222
  return server;
@@ -1641,13 +2230,45 @@ function readOption(args, option) {
1641
2230
  if (!value || value.startsWith("--")) throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `${option} requires a value.`);
1642
2231
  return value;
1643
2232
  }
2233
+ function printUsage() {
2234
+ process.stdout.write(
2235
+ "AI Hub Agent Trade MCP\n\nUsage:\n ai-hub-trade-mcp [--profile <name>] [--read-only]\n ai-hub-trade-mcp setup --client <client> [--profile <name>]\n\nThe default command starts the local stdio MCP server.\nUse setup to register it with Cursor, Claude Desktop, Claude Code, or Codex.\n"
2236
+ );
2237
+ }
1644
2238
  async function main(argv) {
2239
+ if (argv.includes("--help") || argv.includes("-h")) {
2240
+ printUsage();
2241
+ return;
2242
+ }
2243
+ if (argv[0] === "setup") {
2244
+ const client = readOption(argv.slice(1), "--client");
2245
+ const profile = readOption(argv.slice(1), "--profile");
2246
+ if (!client) {
2247
+ printMcpSetupUsage();
2248
+ return;
2249
+ }
2250
+ if (!SUPPORTED_MCP_CLIENTS.includes(client)) {
2251
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `Unknown MCP client "${client}". Supported clients: ${SUPPORTED_MCP_CLIENTS.join(", ")}.`);
2252
+ }
2253
+ const entrypoint = process.argv[1];
2254
+ if (!entrypoint) throw new AiHubError("AI_HUB_UNEXPECTED_ERROR", "Cannot locate the local MCP server entrypoint for client setup.");
2255
+ runMcpSetup({ client, profile, launch: { command: process.execPath, args: [entrypoint] } });
2256
+ return;
2257
+ }
1645
2258
  const profileName = readOption(argv, "--profile");
1646
2259
  const readOnly = argv.includes("--read-only");
1647
2260
  const server = createServer(profileName, readOnly);
1648
2261
  await server.connect(new StdioServerTransport());
1649
2262
  }
1650
- main(process.argv.slice(2)).catch((error) => {
2263
+ function isDirectExecution() {
2264
+ if (!process.argv[1]) return false;
2265
+ try {
2266
+ return realpathSync(process.argv[1]) === fileURLToPath(import.meta.url);
2267
+ } catch {
2268
+ return false;
2269
+ }
2270
+ }
2271
+ if (isDirectExecution()) main(process.argv.slice(2)).catch((error) => {
1651
2272
  const payload = error instanceof AiHubError ? { code: error.code, message: error.message } : { code: "AI_HUB_UNEXPECTED_ERROR", message: error instanceof Error ? error.message : "Unexpected error" };
1652
2273
  process.stderr.write(`${JSON.stringify(payload)}
1653
2274
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aihubspot/agent-trade-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.3",
4
4
  "description": "Local stdio MCP server for AI Hub spot trading tools",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -14,12 +14,11 @@
14
14
  "scripts": {
15
15
  "build": "tsup",
16
16
  "typecheck": "tsc --noEmit",
17
- "test": "tsx --test test/*.test.ts",
17
+ "test": "TSX_TSCONFIG_PATH=./test/tsconfig.json tsx --test test/*.test.ts",
18
18
  "clean": "rm -rf dist"
19
19
  },
20
20
  "dependencies": {
21
21
  "@modelcontextprotocol/sdk": "^1.26.0",
22
- "keytar": "^7.9.0",
23
22
  "smol-toml": "^1.3.4"
24
23
  }
25
24
  }