@aihubspot/agent-trade-mcp 0.1.0 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +15 -0
  2. package/dist/index.js +367 -61
  3. package/package.json +2 -3
package/README.md CHANGED
@@ -3,3 +3,18 @@
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`.
package/dist/index.js CHANGED
@@ -12,12 +12,30 @@ var AiHubError = class extends Error {
12
12
  this.code = code;
13
13
  }
14
14
  };
15
+ var OpenApiBusinessError = class extends AiHubError {
16
+ constructor(diagnosis) {
17
+ super(
18
+ "AI_HUB_OPENAPI_BUSINESS_ERROR",
19
+ `OpenAPI business error ${diagnosis.upstreamCode}: ${diagnosis.upstreamMessage}`
20
+ );
21
+ this.diagnosis = diagnosis;
22
+ this.name = "OpenApiBusinessError";
23
+ }
24
+ diagnosis;
25
+ };
26
+ function toAiHubErrorPayload(error) {
27
+ if (error instanceof OpenApiBusinessError) {
28
+ return { code: error.code, ...error.diagnosis };
29
+ }
30
+ if (error instanceof AiHubError) {
31
+ return { code: error.code, message: error.message };
32
+ }
33
+ return { code: "AI_HUB_UNEXPECTED_ERROR", message: error instanceof Error ? error.message : "Unexpected error" };
34
+ }
15
35
 
16
36
  // ../core/src/credential.ts
17
37
  import { createHash } from "crypto";
18
- import keytar from "keytar";
19
- var SERVICE_NAME = "ai-hub-agent-trade";
20
- function validate(credentials) {
38
+ function validateApiCredentials(credentials) {
21
39
  const apiKey = credentials.apiKey.trim();
22
40
  const secretKey = credentials.secretKey.trim();
23
41
  if (!apiKey || !secretKey) {
@@ -28,28 +46,11 @@ function validate(credentials) {
28
46
  function versionOf(credentials) {
29
47
  return createHash("sha256").update(`${credentials.apiKey}\0${credentials.secretKey}`).digest("hex");
30
48
  }
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
- };
49
+ function loadStoredCredentials(apiKey, secretKey) {
50
+ if (apiKey === void 0 && secretKey === void 0) return void 0;
51
+ const credentials = validateApiCredentials({ apiKey: apiKey ?? "", secretKey: secretKey ?? "" });
52
+ return { ...credentials, credentialVersion: versionOf(credentials) };
53
+ }
53
54
 
54
55
  // ../core/src/confirmation.ts
55
56
  import { createHash as createHash2, randomUUID } from "crypto";
@@ -109,6 +110,100 @@ var ConfirmationService = class {
109
110
 
110
111
  // ../core/src/openapi.ts
111
112
  import { createHmac } from "crypto";
113
+
114
+ // ../core/src/openapi-error-catalog.ts
115
+ var globalErrors = {
116
+ "-1002": { reason: "API_KEY_REQUIRED", suggestedAction: "Configure credentials for the selected profile, then retry the read request." },
117
+ "-1003": { reason: "RATE_LIMITED", suggestedAction: "Wait before retrying. Reduce request frequency and do not retry a write automatically.", retryable: true },
118
+ "-1006": { reason: "WRITE_STATUS_UNKNOWN", suggestedAction: "Do not retry automatically. Query the affected order or history record before taking another action.", writeOutcomeUnknown: true },
119
+ "-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 },
120
+ "-1015": { reason: "TOO_MANY_ORDERS", suggestedAction: "Reduce the number of active or requested orders before retrying." },
121
+ "-1020": { reason: "UNSUPPORTED_OPERATION", suggestedAction: "Use a supported command and do not substitute another operation automatically." },
122
+ "-1021": { reason: "INVALID_TIMESTAMP", suggestedAction: "Synchronize the local system clock, then retry the request." },
123
+ "-1022": { reason: "INVALID_SIGNATURE", suggestedAction: "Verify the selected profile credentials and signing configuration, then retry." },
124
+ "-1023": { reason: "TIMESTAMP_REQUIRED", suggestedAction: "Use the AI Hub CLI or MCP client so the request timestamp is supplied automatically." },
125
+ "-1024": { reason: "SIGNATURE_REQUIRED", suggestedAction: "Use the AI Hub CLI or MCP client so the request is signed automatically." },
126
+ "-1100": { reason: "INVALID_REQUEST_CHARACTERS", suggestedAction: "Correct the invalid request value and retry." },
127
+ "-1102": { reason: "MISSING_OR_MALFORMED_PARAMETER", suggestedAction: "Provide every required parameter in the documented format." },
128
+ "-1103": { reason: "UNKNOWN_PARAMETER", suggestedAction: "Remove unsupported parameters and use the CLI command reference." },
129
+ "-1111": { reason: "INVALID_PRECISION", suggestedAction: "Use the symbol's allowed price and quantity precision." },
130
+ "-1121": { reason: "INVALID_SYMBOL", suggestedAction: "Query supported symbols and use an exact symbol value." },
131
+ "-1136": { reason: "ORDER_QUANTITY_TOO_SMALL", suggestedAction: "Increase the order quantity to the symbol minimum." },
132
+ "-1138": { reason: "ORDER_PRICE_OUT_OF_RANGE", suggestedAction: "Use a price inside the allowed range, then create a new preview." },
133
+ "-1139": { reason: "MARKET_ORDER_UNSUPPORTED", suggestedAction: "Use a supported order type for the selected symbol." },
134
+ "-1145": { reason: "ORDER_CANNOT_BE_CANCELLED", suggestedAction: "Query the latest order status before attempting another cancellation." },
135
+ "-2013": { reason: "ORDER_NOT_FOUND", suggestedAction: "Verify the symbol and order ID, then query open orders if needed." },
136
+ "-2014": { reason: "INVALID_API_KEY_FORMAT", suggestedAction: "Replace the configured API key with a valid value." },
137
+ "-2015": { reason: "API_KEY_REJECTED", suggestedAction: "Verify API key permissions, IP restrictions, and the selected tenant profile." },
138
+ "-2016": { reason: "TRADING_DISABLED", suggestedAction: "The account is restricted. Contact the platform administrator before trying again." },
139
+ "-2017": { reason: "INSUFFICIENT_BALANCE", suggestedAction: "Check the available balance and reduce the requested amount before creating a new preview." },
140
+ "-2018": { reason: "DUPLICATE_WITHDRAWAL_REQUEST", suggestedAction: "Use a new withdrawal order ID or query withdrawal history before retrying." },
141
+ "-2021": { reason: "IDENTITY_VERIFICATION_REQUIRED", suggestedAction: "Complete the account verification required by the tenant before retrying." },
142
+ "-2022": { reason: "LIMIT_ORDER_VOLUME_TOO_SMALL", suggestedAction: "Increase the limit-order volume to the returned minimum." },
143
+ "-2023": { reason: "MARKET_ORDER_VOLUME_TOO_SMALL", suggestedAction: "Increase the market-order sell volume to the returned minimum." },
144
+ "-2024": { reason: "LIMIT_ORDER_PRICE_TOO_SMALL", suggestedAction: "Increase the limit-order price to the returned minimum." },
145
+ "-2025": { reason: "MARKET_ORDER_AMOUNT_TOO_SMALL", suggestedAction: "Increase the market-order buy amount to the returned minimum." },
146
+ "-2026": { reason: "BUY_AMOUNT_TOO_LARGE", suggestedAction: "Reduce the buy amount before creating a new preview." },
147
+ "-2027": { reason: "SELL_AMOUNT_TOO_LARGE", suggestedAction: "Reduce the sell amount before creating a new preview." },
148
+ "-2029": { reason: "LIMIT_ORDER_NOTIONAL_TOO_SMALL", suggestedAction: "Increase the limit-order notional amount to the returned minimum." },
149
+ "-2030": { reason: "TRANSFER_NOT_FOUND", suggestedAction: "Verify the transfer ID, or query by the documented source and destination accounts." },
150
+ "-2031": { reason: "TRANSFER_TYPE_UNSUPPORTED", suggestedAction: "Use a supported source and destination account combination." },
151
+ "-2033": { reason: "ASSET_TRANSFER_FAILED", suggestedAction: "Query transfer history before attempting another transfer." },
152
+ "-2034": { reason: "SUB_ACCOUNT_RELATION_NOT_FOUND", suggestedAction: "Use a sub-account that belongs to the authenticated parent account." },
153
+ "-2035": { reason: "SUB_ACCOUNT_API_KEY_NOT_FOUND", suggestedAction: "Verify the sub-account API key identifier before retrying." },
154
+ "-2036": { reason: "SUB_ACCOUNT_PERMISSION_DENIED", suggestedAction: "Use a parent-account credential with the required sub-account permission." },
155
+ "-2037": { reason: "SUB_ACCOUNT_CREATION_FAILED", suggestedAction: "Check the sub-account limit and requested identifier before creating a new preview." },
156
+ "-2038": { reason: "SUB_ACCOUNT_UPDATE_FAILED", suggestedAction: "Query the latest sub-account state before retrying the update." },
157
+ "-2040": { reason: "SUB_ACCOUNT_CREDENTIAL_REQUIRED", suggestedAction: "Use the sub-account credential required by this endpoint." },
158
+ "10001": { reason: "USER_ACCOUNT_LOCKED", suggestedAction: "The account is restricted. Contact the platform administrator." },
159
+ "10004": { reason: "USER_NOT_AUTHENTICATED", suggestedAction: "Configure valid credentials for the selected profile." },
160
+ "10005": { reason: "PERMISSION_DENIED", suggestedAction: "Verify the API key permissions and the tenant feature permission." },
161
+ "10006": { reason: "TENANT_PERMISSION_DENIED", suggestedAction: "Ask the tenant administrator to enable this capability." },
162
+ "21018": { reason: "SUB_ACCOUNT_NOT_FOUND", suggestedAction: "List sub-accounts and use an existing sub-account ID." },
163
+ "21019": { reason: "COIN_NOT_FOUND", suggestedAction: "Query supported coins and use an exact coin symbol." },
164
+ "21020": { reason: "SYMBOL_NOT_FOUND", suggestedAction: "Query supported symbols and use an exact symbol value." },
165
+ "21022": { reason: "ORDER_NOT_FOUND", suggestedAction: "Verify the order identifier and symbol, then query current orders if needed." },
166
+ "21024": { reason: "SUB_ACCOUNT_FEATURE_PERMISSION_DENIED", suggestedAction: "Enable the required sub-account feature permission before retrying." },
167
+ "21025": { reason: "SUB_ACCOUNT_LIMIT_REACHED", suggestedAction: "The parent account has reached its sub-account limit." }
168
+ };
169
+ var endpointErrors = {
170
+ "/sapi/v1/withdraw/query:10005": { reason: "WITHDRAW_HISTORY_PERMISSION_DENIED", suggestedAction: "Enable the withdrawal-history permission for this API key, then retry." },
171
+ "/sapi/v1/withdraw/query:-2015": { reason: "WITHDRAW_HISTORY_TRUSTED_IP_REQUIRED", suggestedAction: "Add a trusted IP to the API key before querying withdrawal history." },
172
+ "/sapi/v1/withdraw/query:-1004": { reason: "WITHDRAW_HISTORY_PARENT_ACCOUNT_REQUIRED", suggestedAction: "Use a parent-account credential for withdrawal history." },
173
+ "/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." },
174
+ "/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." },
175
+ "/sapi/v1/asset/subaccount/transfer_query:-2040": { reason: "PARENT_TRANSFER_SUB_ACCOUNT_REQUIRED", suggestedAction: "Run this query with the relevant sub-account credential." }
176
+ };
177
+ function normalizeCode(value) {
178
+ return String(value).trim();
179
+ }
180
+ function diagnoseOpenApiBusinessError(path2, upstreamCode, upstreamMessage) {
181
+ const code = normalizeCode(upstreamCode);
182
+ const definition = endpointErrors[`${path2}:${code}`] ?? globalErrors[code];
183
+ if (!definition) {
184
+ return {
185
+ upstreamCode: code,
186
+ upstreamMessage,
187
+ reason: "UNKNOWN_UPSTREAM_CODE",
188
+ suggestedAction: "Review the upstream message and endpoint. Do not retry a state-changing request automatically.",
189
+ retryable: false,
190
+ writeOutcomeUnknown: false
191
+ };
192
+ }
193
+ return {
194
+ upstreamCode: code,
195
+ upstreamMessage,
196
+ reason: definition.reason,
197
+ suggestedAction: definition.suggestedAction,
198
+ retryable: definition.retryable ?? false,
199
+ writeOutcomeUnknown: definition.writeOutcomeUnknown ?? false
200
+ };
201
+ }
202
+ function isOpenApiSuccessCode(value) {
203
+ return value === 0 || value === "0";
204
+ }
205
+
206
+ // ../core/src/openapi.ts
112
207
  function parseJsonPreservingLargeIntegers(raw) {
113
208
  let transformed = "";
114
209
  let inString = false;
@@ -155,13 +250,13 @@ function queryString(values) {
155
250
  }
156
251
  return params.toString();
157
252
  }
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}`;
253
+ function joinUrl(baseUrl, path2, query) {
254
+ if (!path2.startsWith("/")) throw new AiHubError("AI_HUB_INVALID_PATH", "OpenAPI paths must start with '/'.");
255
+ const url = `${baseUrl.replace(/\/+$/, "")}${path2}`;
161
256
  return query ? `${url}?${query}` : url;
162
257
  }
163
- function signRequest(timestamp, method, path, secretKey, query, body) {
164
- const payload = `${timestamp}${method.toUpperCase()}${path}${query ? `?${query}` : ""}${body ?? ""}`;
258
+ function signRequest(timestamp, method, path2, secretKey, query, body) {
259
+ const payload = `${timestamp}${method.toUpperCase()}${path2}${query ? `?${query}` : ""}${body ?? ""}`;
165
260
  return createHmac("sha256", secretKey).update(payload).digest("hex");
166
261
  }
167
262
  var AiHubSpotApi = class {
@@ -236,13 +331,13 @@ var AiHubSpotApi = class {
236
331
  * defined by the API document. Keep endpoint selection in the individual Tool
237
332
  * definition; never expose this method directly through CLI or MCP.
238
333
  */
239
- signedGet(path, query, credentials) {
240
- return this.request("GET", path, query, void 0, credentials);
334
+ signedGet(path2, query, credentials) {
335
+ return this.request("GET", path2, query, void 0, credentials);
241
336
  }
242
- signedPost(path, body, credentials) {
243
- return this.request("POST", path, {}, body, credentials);
337
+ signedPost(path2, body, credentials) {
338
+ return this.request("POST", path2, {}, body, credentials);
244
339
  }
245
- async request(method, path, query = {}, body, credentials) {
340
+ async request(method, path2, query = {}, body, credentials) {
246
341
  const encodedQuery = queryString(query);
247
342
  const bodyString = body ? JSON.stringify(body) : void 0;
248
343
  const headers = {
@@ -255,11 +350,11 @@ var AiHubSpotApi = class {
255
350
  const timestamp = Date.now().toString();
256
351
  headers["X-CH-APIKEY"] = credentials.apiKey;
257
352
  headers["X-CH-TS"] = timestamp;
258
- headers["X-CH-SIGN"] = signRequest(timestamp, method, path, credentials.secretKey, encodedQuery || void 0, bodyString);
353
+ headers["X-CH-SIGN"] = signRequest(timestamp, method, path2, credentials.secretKey, encodedQuery || void 0, bodyString);
259
354
  }
260
355
  let response;
261
356
  try {
262
- response = await fetch(joinUrl(this.baseUrl, path, encodedQuery), {
357
+ response = await fetch(joinUrl(this.baseUrl, path2, encodedQuery), {
263
358
  method,
264
359
  headers,
265
360
  body: bodyString,
@@ -277,8 +372,18 @@ var AiHubSpotApi = class {
277
372
  throw new AiHubError("AI_HUB_OPENAPI_HTTP_ERROR", `OpenAPI returned HTTP ${response.status}.`);
278
373
  }
279
374
  try {
280
- return parseJsonPreservingLargeIntegers(raw);
281
- } catch {
375
+ const parsed = parseJsonPreservingLargeIntegers(raw);
376
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
377
+ const payload = parsed;
378
+ if (Object.hasOwn(payload, "code") && !isOpenApiSuccessCode(payload.code)) {
379
+ const upstreamCode = typeof payload.code === "string" || typeof payload.code === "number" ? payload.code : String(payload.code);
380
+ const upstreamMessage = typeof payload.msg === "string" ? payload.msg : "OpenAPI returned a business failure without a message.";
381
+ throw new OpenApiBusinessError(diagnoseOpenApiBusinessError(path2, upstreamCode, upstreamMessage));
382
+ }
383
+ }
384
+ return parsed;
385
+ } catch (error) {
386
+ if (error instanceof OpenApiBusinessError) throw error;
282
387
  throw new AiHubError("AI_HUB_OPENAPI_INVALID_RESPONSE", "OpenAPI returned a non-JSON response.");
283
388
  }
284
389
  }
@@ -377,7 +482,11 @@ var ConfigStore = class {
377
482
  const config = await this.read();
378
483
  const normalizedUrl = await normalizeOpenApiBaseUrl(openApiBaseUrl);
379
484
  const existing = config.profiles[name];
380
- config.profiles[name] = { openapi_base_url: normalizedUrl, ...existing?.credential_ref ? { credential_ref: existing.credential_ref } : {} };
485
+ config.profiles[name] = {
486
+ openapi_base_url: normalizedUrl,
487
+ ...existing?.api_key ? { api_key: existing.api_key } : {},
488
+ ...existing?.secret_key ? { secret_key: existing.secret_key } : {}
489
+ };
381
490
  if (Object.keys(config.profiles).length === 1) config.default_profile = name;
382
491
  await this.write(config);
383
492
  return this.resolveFrom(config, name);
@@ -386,6 +495,12 @@ var ConfigStore = class {
386
495
  const config = await this.read();
387
496
  return this.resolveFrom(config, requestedName ?? config.default_profile);
388
497
  }
498
+ async getCredentials(requestedName) {
499
+ const config = await this.read();
500
+ const name = requestedName ?? config.default_profile;
501
+ const profile = this.profileFrom(config, name);
502
+ return loadStoredCredentials(profile.api_key, profile.secret_key);
503
+ }
389
504
  async removeProfile(name) {
390
505
  validateProfileName(name);
391
506
  const config = await this.read();
@@ -394,26 +509,31 @@ var ConfigStore = class {
394
509
  if (config.default_profile === name) config.default_profile = Object.keys(config.profiles)[0] ?? "default";
395
510
  await this.write(config);
396
511
  }
397
- async setCredentialRef(name, credentialRef) {
512
+ async setCredentials(name, credentials) {
398
513
  validateProfileName(name);
399
514
  const config = await this.read();
400
515
  const profile = config.profiles[name];
401
516
  if (!profile) throw new AiHubError("AI_HUB_PROFILE_NOT_FOUND", `Profile "${name}" does not exist.`);
402
- config.profiles[name] = { ...profile, credential_ref: credentialRef };
517
+ const value = validateApiCredentials(credentials);
518
+ config.profiles[name] = { openapi_base_url: profile.openapi_base_url, api_key: value.apiKey, secret_key: value.secretKey };
403
519
  await this.write(config);
404
520
  return this.resolveFrom(config, name);
405
521
  }
406
522
  resolveFrom(config, name) {
407
523
  validateProfileName(name);
408
- const profile = config.profiles[name];
409
- if (!profile) throw new AiHubError("AI_HUB_PROFILE_NOT_FOUND", `Profile "${name}" does not exist.`);
524
+ const profile = this.profileFrom(config, name);
410
525
  return {
411
526
  name,
412
527
  openApiBaseUrl: profile.openapi_base_url,
413
- credentialRef: profile.credential_ref,
414
528
  configVersion: versionOf2(config, name)
415
529
  };
416
530
  }
531
+ profileFrom(config, name) {
532
+ validateProfileName(name);
533
+ const profile = config.profiles[name];
534
+ if (!profile) throw new AiHubError("AI_HUB_PROFILE_NOT_FOUND", `Profile "${name}" does not exist.`);
535
+ return profile;
536
+ }
417
537
  async write(config) {
418
538
  const directory = dirname(this.filePath);
419
539
  await mkdir(directory, { recursive: true, mode: 448 });
@@ -428,8 +548,9 @@ var ConfigStore = class {
428
548
 
429
549
  // ../core/src/tools/execution-context.ts
430
550
  async function createToolExecutionContext(profileName) {
431
- const profile = await new ConfigStore().showProfile(profileName);
432
- const credentials = await new CredentialStore().get(profile.name);
551
+ const store = new ConfigStore();
552
+ const profile = await store.showProfile(profileName);
553
+ const credentials = await store.getCredentials(profile.name);
433
554
  return { profile, credentials, api: new AiHubSpotApi(profile.openApiBaseUrl) };
434
555
  }
435
556
  function confirmationContext(context) {
@@ -485,7 +606,7 @@ var accountTools = [
485
606
  operation: "read",
486
607
  riskLevel: "low",
487
608
  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"],
609
+ 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
610
  validate: (input) => strictObject(input, []),
490
611
  handler: async (_input, context) => {
491
612
  if (!context.credentials) throw new AiHubError("AI_HUB_CREDENTIAL_NOT_CONFIGURED", `Credentials are not configured for profile "${context.profile.name}".`);
@@ -496,7 +617,7 @@ var accountTools = [
496
617
 
497
618
  // ../core/src/tools/tool-utils.ts
498
619
  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"];
620
+ 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
621
  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
622
  function signed(context) {
502
623
  if (!context.credentials) throw new AiHubError("AI_HUB_CREDENTIAL_NOT_CONFIGURED", `Credentials are not configured for profile "${context.profile.name}".`);
@@ -744,7 +865,7 @@ var assetTools = [
744
865
  ];
745
866
 
746
867
  // ../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"];
868
+ 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
869
  var marketTools = [
749
870
  {
750
871
  name: "market_ping",
@@ -756,7 +877,7 @@ var marketTools = [
756
877
  operation: "read",
757
878
  riskLevel: "low",
758
879
  inputSchema: { type: "object", additionalProperties: false },
759
- errorCodes: ["AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE"],
880
+ errorCodes: ["AI_HUB_OPENAPI_NETWORK_ERROR", "AI_HUB_OPENAPI_HTTP_ERROR", "AI_HUB_OPENAPI_INVALID_RESPONSE", "AI_HUB_OPENAPI_BUSINESS_ERROR"],
760
881
  validate: (input) => {
761
882
  strictObject(input, []);
762
883
  return {};
@@ -973,7 +1094,7 @@ var marginTools = [
973
1094
 
974
1095
  // ../core/src/tools/order-tools.ts
975
1096
  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"];
1097
+ 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
1098
  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
1099
  var decimal = /^(?:0|[1-9]\d*)(?:\.\d+)?$/;
979
1100
  function positiveDecimal2(value, name) {
@@ -1446,12 +1567,12 @@ var ToolRegistry = class {
1446
1567
  toolsByCliPath = /* @__PURE__ */ new Map();
1447
1568
  constructor(tools = allTools) {
1448
1569
  for (const tool of tools) {
1449
- const path = tool.cliPath.join(" ");
1450
- if (this.toolsByName.has(tool.name) || this.toolsByCliPath.has(path)) {
1570
+ const path2 = tool.cliPath.join(" ");
1571
+ if (this.toolsByName.has(tool.name) || this.toolsByCliPath.has(path2)) {
1451
1572
  throw new AiHubError("AI_HUB_TOOL_DUPLICATE", `Duplicate tool registration: ${tool.name}.`);
1452
1573
  }
1453
1574
  this.toolsByName.set(tool.name, tool);
1454
- this.toolsByCliPath.set(path, tool);
1575
+ this.toolsByCliPath.set(path2, tool);
1455
1576
  }
1456
1577
  }
1457
1578
  list(options = {}) {
@@ -1464,10 +1585,10 @@ var ToolRegistry = class {
1464
1585
  }
1465
1586
  return tool;
1466
1587
  }
1467
- byCliPath(path, options = {}) {
1468
- const tool = this.toolsByCliPath.get(path.join(" "));
1588
+ byCliPath(path2, options = {}) {
1589
+ const tool = this.toolsByCliPath.get(path2.join(" "));
1469
1590
  if (!tool || options.readOnly && tool.operation !== "read") {
1470
- throw new AiHubError("AI_HUB_TOOL_NOT_AVAILABLE", `Command "${path.join(" ")}" is not available.`);
1591
+ throw new AiHubError("AI_HUB_TOOL_NOT_AVAILABLE", `Command "${path2.join(" ")}" is not available.`);
1471
1592
  }
1472
1593
  return tool;
1473
1594
  }
@@ -1532,6 +1653,176 @@ var ToolWriteExecutor = class {
1532
1653
  }
1533
1654
  };
1534
1655
 
1656
+ // ../core/src/setup.ts
1657
+ import { execFileSync } from "child_process";
1658
+ import * as fs from "fs";
1659
+ import * as os from "os";
1660
+ import * as path from "path";
1661
+ var MCP_BINARY = "ai-hub-trade-mcp";
1662
+ var MCP_CLIENT_NAMES = {
1663
+ cursor: "Cursor",
1664
+ "claude-desktop": "Claude Desktop",
1665
+ "claude-code": "Claude Code",
1666
+ codex: "Codex"
1667
+ };
1668
+ var SUPPORTED_MCP_CLIENTS = Object.keys(MCP_CLIENT_NAMES);
1669
+ function runtime() {
1670
+ const home = os.homedir();
1671
+ return {
1672
+ home,
1673
+ platform: process.platform,
1674
+ cwd: process.cwd(),
1675
+ appData: process.env.APPDATA,
1676
+ localAppData: process.env.LOCALAPPDATA,
1677
+ xdgConfigHome: process.env.XDG_CONFIG_HOME
1678
+ };
1679
+ }
1680
+ function windowsAppData(value, home) {
1681
+ return value ?? path.join(home, "AppData", "Roaming");
1682
+ }
1683
+ function findMicrosoftStoreClaudePath(value, home) {
1684
+ const packagesDirectory = path.join(value ?? path.join(home, "AppData", "Local"), "Packages");
1685
+ try {
1686
+ const packageName = fs.readdirSync(packagesDirectory).find((entry) => entry.startsWith("Claude_"));
1687
+ if (!packageName) return void 0;
1688
+ const configPath = path.join(
1689
+ packagesDirectory,
1690
+ packageName,
1691
+ "LocalCache",
1692
+ "Roaming",
1693
+ "Claude",
1694
+ "claude_desktop_config.json"
1695
+ );
1696
+ return fs.existsSync(configPath) || fs.existsSync(path.dirname(configPath)) ? configPath : void 0;
1697
+ } catch {
1698
+ return void 0;
1699
+ }
1700
+ }
1701
+ function getMcpClientConfigPath(client, value = runtime()) {
1702
+ if (client === "cursor") return path.join(value.home, ".cursor", "mcp.json");
1703
+ if (value.platform === "win32") {
1704
+ return findMicrosoftStoreClaudePath(value.localAppData, value.home) ?? path.join(windowsAppData(value.appData, value.home), "Claude", "claude_desktop_config.json");
1705
+ }
1706
+ if (value.platform === "darwin") {
1707
+ return path.join(value.home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1708
+ }
1709
+ return path.join(value.xdgConfigHome ?? path.join(value.home, ".config"), "Claude", "claude_desktop_config.json");
1710
+ }
1711
+ function buildServerSpec(profile, launch) {
1712
+ return {
1713
+ name: serverName(profile),
1714
+ command: launch.command,
1715
+ args: [...launch.args, ...profile ? ["--profile", profile] : []]
1716
+ };
1717
+ }
1718
+ function serverName(profile) {
1719
+ return profile ? `${MCP_BINARY}-${profile}` : MCP_BINARY;
1720
+ }
1721
+ function isRecord(value) {
1722
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1723
+ }
1724
+ function mergeJsonMcpConfig(configPath, server) {
1725
+ const directory = path.dirname(configPath);
1726
+ fs.mkdirSync(directory, { recursive: true });
1727
+ const configExists = fs.existsSync(configPath);
1728
+ let config = {};
1729
+ if (configExists) {
1730
+ const raw = fs.readFileSync(configPath, "utf8");
1731
+ try {
1732
+ const parsed = JSON.parse(raw);
1733
+ if (!isRecord(parsed)) throw new Error("root must be a JSON object");
1734
+ config = parsed;
1735
+ } catch {
1736
+ throw new Error(`Failed to parse existing MCP configuration at ${configPath}. No changes were made.`);
1737
+ }
1738
+ }
1739
+ if (config.mcpServers === void 0) {
1740
+ config.mcpServers = {};
1741
+ }
1742
+ if (!isRecord(config.mcpServers)) {
1743
+ throw new Error(`Existing MCP configuration at ${configPath} has an invalid mcpServers object. No changes were made.`);
1744
+ }
1745
+ if (configExists) {
1746
+ const backupPath = `${configPath}.bak`;
1747
+ if (!fs.existsSync(backupPath)) {
1748
+ fs.copyFileSync(configPath, backupPath);
1749
+ process.stdout.write(`Backup created: ${backupPath}
1750
+ `);
1751
+ }
1752
+ }
1753
+ config.mcpServers[server.name] = { command: server.command, args: server.args };
1754
+ const mode = configExists ? fs.statSync(configPath).mode & 511 : 384;
1755
+ const temporaryPath = `${configPath}.${process.pid}.tmp`;
1756
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(config, null, 2)}
1757
+ `, { encoding: "utf8", mode });
1758
+ fs.chmodSync(temporaryPath, mode);
1759
+ fs.renameSync(temporaryPath, configPath);
1760
+ }
1761
+ function executeClientRegistration(command, args, value) {
1762
+ process.stdout.write(`Running: ${command} ${args.join(" ")}
1763
+ `);
1764
+ if (value.executeClientCommand) {
1765
+ value.executeClientCommand(command, args);
1766
+ return;
1767
+ }
1768
+ execFileSync(command, args, { stdio: "inherit" });
1769
+ }
1770
+ function jsonFileAdapter(id) {
1771
+ return {
1772
+ id,
1773
+ name: MCP_CLIENT_NAMES[id],
1774
+ install(server, value) {
1775
+ mergeJsonMcpConfig(getMcpClientConfigPath(id, value), server);
1776
+ process.stdout.write(`Configured ${this.name}: ${getMcpClientConfigPath(id, value)}
1777
+ Restart ${this.name} to apply changes.
1778
+ `);
1779
+ }
1780
+ };
1781
+ }
1782
+ function cliAdapter(id) {
1783
+ const command = id === "claude-code" ? "claude" : "codex";
1784
+ return {
1785
+ id,
1786
+ name: MCP_CLIENT_NAMES[id],
1787
+ install(server, value) {
1788
+ const args = [
1789
+ "mcp",
1790
+ "add",
1791
+ ...id === "claude-code" ? ["--scope", "user", "--transport", "stdio"] : [],
1792
+ server.name,
1793
+ "--",
1794
+ server.command,
1795
+ ...server.args
1796
+ ];
1797
+ executeClientRegistration(command, args, value);
1798
+ process.stdout.write(`Configured ${this.name} with local stdio MCP server "${server.name}".
1799
+ `);
1800
+ }
1801
+ };
1802
+ }
1803
+ var MCP_CLIENT_ADAPTERS = {
1804
+ cursor: jsonFileAdapter("cursor"),
1805
+ "claude-desktop": jsonFileAdapter("claude-desktop"),
1806
+ "claude-code": cliAdapter("claude-code"),
1807
+ codex: cliAdapter("codex")
1808
+ };
1809
+ function runMcpSetup(options, value = runtime()) {
1810
+ const adapter = MCP_CLIENT_ADAPTERS[options.client];
1811
+ if (!adapter) {
1812
+ throw new Error(`Unknown MCP client "${options.client}". Supported clients: ${SUPPORTED_MCP_CLIENTS.join(", ")}.`);
1813
+ }
1814
+ const profile = options.profile ? validateProfileName(options.profile) : void 0;
1815
+ adapter.install(buildServerSpec(profile, options.launch), value);
1816
+ }
1817
+ function printMcpSetupUsage() {
1818
+ process.stdout.write(
1819
+ `Usage: ${MCP_BINARY} setup --client <client> [--profile <name>]
1820
+
1821
+ Supported clients:
1822
+ ` + SUPPORTED_MCP_CLIENTS.map((client) => ` ${client.padEnd(16)} ${MCP_CLIENT_NAMES[client]}`).join("\n") + "\n"
1823
+ );
1824
+ }
1825
+
1535
1826
  // src/server.ts
1536
1827
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1537
1828
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -1540,8 +1831,8 @@ var CONFIRM_ACTION_TOOL = "confirm_action";
1540
1831
  function result(data) {
1541
1832
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1542
1833
  }
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" };
1834
+ function toMcpErrorResult(error) {
1835
+ const payload = toAiHubErrorPayload(error);
1545
1836
  return { isError: true, content: [{ type: "text", text: JSON.stringify({ ok: false, ...payload }) }] };
1546
1837
  }
1547
1838
  function toMcpTool(tool) {
@@ -1627,7 +1918,7 @@ function createServer(profileName, readOnly) {
1627
1918
  const data = await registry.execute(request.params.name, request.params.arguments ?? {}, context, { readOnly });
1628
1919
  return result({ ok: true, data });
1629
1920
  } catch (error) {
1630
- return errorResult(error);
1921
+ return toMcpErrorResult(error);
1631
1922
  }
1632
1923
  });
1633
1924
  return server;
@@ -1642,6 +1933,21 @@ function readOption(args, option) {
1642
1933
  return value;
1643
1934
  }
1644
1935
  async function main(argv) {
1936
+ if (argv[0] === "setup") {
1937
+ const client = readOption(argv.slice(1), "--client");
1938
+ const profile = readOption(argv.slice(1), "--profile");
1939
+ if (!client) {
1940
+ printMcpSetupUsage();
1941
+ return;
1942
+ }
1943
+ if (!SUPPORTED_MCP_CLIENTS.includes(client)) {
1944
+ throw new AiHubError("AI_HUB_INVALID_ARGUMENT", `Unknown MCP client "${client}". Supported clients: ${SUPPORTED_MCP_CLIENTS.join(", ")}.`);
1945
+ }
1946
+ const entrypoint = process.argv[1];
1947
+ if (!entrypoint) throw new AiHubError("AI_HUB_UNEXPECTED_ERROR", "Cannot locate the local MCP server entrypoint for client setup.");
1948
+ runMcpSetup({ client, profile, launch: { command: process.execPath, args: [entrypoint] } });
1949
+ return;
1950
+ }
1645
1951
  const profileName = readOption(argv, "--profile");
1646
1952
  const readOnly = argv.includes("--read-only");
1647
1953
  const server = createServer(profileName, readOnly);
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.1",
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
  }