@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.
- package/README.md +21 -0
- package/dist/index.js +755 -134
- 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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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,
|
|
159
|
-
if (!
|
|
160
|
-
const url = `${baseUrl.replace(/\/+$/, "")}${
|
|
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,
|
|
164
|
-
const payload = `${timestamp}${method.toUpperCase()}${
|
|
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(
|
|
240
|
-
return this.request("GET",
|
|
336
|
+
signedGet(path2, query, credentials) {
|
|
337
|
+
return this.request("GET", path2, query, void 0, credentials);
|
|
241
338
|
}
|
|
242
|
-
signedPost(
|
|
243
|
-
return this.request("POST",
|
|
339
|
+
signedPost(path2, body, credentials) {
|
|
340
|
+
return this.request("POST", path2, {}, body, credentials);
|
|
244
341
|
}
|
|
245
|
-
async request(method,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
281
|
-
|
|
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] = {
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
432
|
-
const
|
|
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
|
|
874
|
-
const value = strictObject(input, ["symbol", "
|
|
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
|
|
877
|
-
|
|
878
|
-
if (type === "MARKET" &&
|
|
879
|
-
|
|
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: "
|
|
938
|
-
title: "
|
|
939
|
-
description: "
|
|
940
|
-
cliPath: ["margin", "order", "
|
|
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" },
|
|
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:
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
1041
|
-
if (!
|
|
1042
|
-
const
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
|
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" },
|
|
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
|
|
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: "
|
|
1164
|
-
title: "
|
|
1165
|
-
description: "
|
|
1166
|
-
cliPath: ["spot", "order", "
|
|
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" },
|
|
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:
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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
|
|
1450
|
-
if (this.toolsByName.has(tool.name) || this.toolsByCliPath.has(
|
|
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(
|
|
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(
|
|
1468
|
-
const tool = this.toolsByCliPath.get(
|
|
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 "${
|
|
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:
|
|
1906
|
+
payload: preflightInput,
|
|
1487
1907
|
context: confirmationContext(context),
|
|
1488
|
-
summary: tool.writeSummary(
|
|
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
|
-
|
|
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
|
|
1544
|
-
const payload = 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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
}
|