@bithumb-official/bithumb-mcp 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,3576 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { parseArgs } from "util";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+
7
+ // ../core/dist/index.js
8
+ import { createHash, createHmac, randomUUID } from "crypto";
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import os from "os";
12
+ import { existsSync } from "fs";
13
+ import { join } from "path";
14
+ import { homedir } from "os";
15
+ import { readFileSync, writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
16
+ import { join as join2, dirname } from "path";
17
+ import { homedir as homedir2 } from "os";
18
+
19
+ // ../../node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/error.js
20
+ function getLineColFromPtr(string, ptr) {
21
+ let lines = string.slice(0, ptr).split(/\r\n|\n|\r/g);
22
+ return [lines.length, lines.pop().length + 1];
23
+ }
24
+ function makeCodeBlock(string, line, column) {
25
+ let lines = string.split(/\r\n|\n|\r/g);
26
+ let codeblock = "";
27
+ let numberLen = (Math.log10(line + 1) | 0) + 1;
28
+ for (let i = line - 1; i <= line + 1; i++) {
29
+ let l = lines[i - 1];
30
+ if (!l)
31
+ continue;
32
+ codeblock += i.toString().padEnd(numberLen, " ");
33
+ codeblock += ": ";
34
+ codeblock += l;
35
+ codeblock += "\n";
36
+ if (i === line) {
37
+ codeblock += " ".repeat(numberLen + column + 2);
38
+ codeblock += "^\n";
39
+ }
40
+ }
41
+ return codeblock;
42
+ }
43
+ var TomlError = class extends Error {
44
+ line;
45
+ column;
46
+ codeblock;
47
+ constructor(message, options) {
48
+ const [line, column] = getLineColFromPtr(options.toml, options.ptr);
49
+ const codeblock = makeCodeBlock(options.toml, line, column);
50
+ super(`Invalid TOML document: ${message}
51
+
52
+ ${codeblock}`, options);
53
+ this.line = line;
54
+ this.column = column;
55
+ this.codeblock = codeblock;
56
+ }
57
+ };
58
+
59
+ // ../../node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/util.js
60
+ function isEscaped(str, ptr) {
61
+ let i = 0;
62
+ while (str[ptr - ++i] === "\\")
63
+ ;
64
+ return --i && i % 2;
65
+ }
66
+ function indexOfNewline(str, start = 0, end = str.length) {
67
+ let idx = str.indexOf("\n", start);
68
+ if (str[idx - 1] === "\r")
69
+ idx--;
70
+ return idx <= end ? idx : -1;
71
+ }
72
+ function skipComment(str, ptr) {
73
+ for (let i = ptr; i < str.length; i++) {
74
+ let c = str[i];
75
+ if (c === "\n")
76
+ return i;
77
+ if (c === "\r" && str[i + 1] === "\n")
78
+ return i + 1;
79
+ if (c < " " && c !== " " || c === "\x7F") {
80
+ throw new TomlError("control characters are not allowed in comments", {
81
+ toml: str,
82
+ ptr
83
+ });
84
+ }
85
+ }
86
+ return str.length;
87
+ }
88
+ function skipVoid(str, ptr, banNewLines, banComments) {
89
+ let c;
90
+ while (1) {
91
+ while ((c = str[ptr]) === " " || c === " " || !banNewLines && (c === "\n" || c === "\r" && str[ptr + 1] === "\n"))
92
+ ptr++;
93
+ if (banComments || c !== "#")
94
+ break;
95
+ ptr = skipComment(str, ptr);
96
+ }
97
+ return ptr;
98
+ }
99
+ function skipUntil(str, ptr, sep, end, banNewLines = false) {
100
+ if (!end) {
101
+ ptr = indexOfNewline(str, ptr);
102
+ return ptr < 0 ? str.length : ptr;
103
+ }
104
+ for (let i = ptr; i < str.length; i++) {
105
+ let c = str[i];
106
+ if (c === "#") {
107
+ i = indexOfNewline(str, i);
108
+ } else if (c === sep) {
109
+ return i + 1;
110
+ } else if (c === end || banNewLines && (c === "\n" || c === "\r" && str[i + 1] === "\n")) {
111
+ return i;
112
+ }
113
+ }
114
+ throw new TomlError("cannot find end of structure", {
115
+ toml: str,
116
+ ptr
117
+ });
118
+ }
119
+ function getStringEnd(str, seek) {
120
+ let first = str[seek];
121
+ let target = first === str[seek + 1] && str[seek + 1] === str[seek + 2] ? str.slice(seek, seek + 3) : first;
122
+ seek += target.length - 1;
123
+ do
124
+ seek = str.indexOf(target, ++seek);
125
+ while (seek > -1 && first !== "'" && isEscaped(str, seek));
126
+ if (seek > -1) {
127
+ seek += target.length;
128
+ if (target.length > 1) {
129
+ if (str[seek] === first)
130
+ seek++;
131
+ if (str[seek] === first)
132
+ seek++;
133
+ }
134
+ }
135
+ return seek;
136
+ }
137
+
138
+ // ../../node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/date.js
139
+ var DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[T ]?(?:(\d{2}):\d{2}(?::\d{2}(?:\.\d+)?)?)?(Z|[-+]\d{2}:\d{2})?$/i;
140
+ var TomlDate = class _TomlDate extends Date {
141
+ #hasDate = false;
142
+ #hasTime = false;
143
+ #offset = null;
144
+ constructor(date) {
145
+ let hasDate = true;
146
+ let hasTime = true;
147
+ let offset = "Z";
148
+ if (typeof date === "string") {
149
+ let match = date.match(DATE_TIME_RE);
150
+ if (match) {
151
+ if (!match[1]) {
152
+ hasDate = false;
153
+ date = `0000-01-01T${date}`;
154
+ }
155
+ hasTime = !!match[2];
156
+ hasTime && date[10] === " " && (date = date.replace(" ", "T"));
157
+ if (match[2] && +match[2] > 23) {
158
+ date = "";
159
+ } else {
160
+ offset = match[3] || null;
161
+ date = date.toUpperCase();
162
+ if (!offset && hasTime)
163
+ date += "Z";
164
+ }
165
+ } else {
166
+ date = "";
167
+ }
168
+ }
169
+ super(date);
170
+ if (!isNaN(this.getTime())) {
171
+ this.#hasDate = hasDate;
172
+ this.#hasTime = hasTime;
173
+ this.#offset = offset;
174
+ }
175
+ }
176
+ isDateTime() {
177
+ return this.#hasDate && this.#hasTime;
178
+ }
179
+ isLocal() {
180
+ return !this.#hasDate || !this.#hasTime || !this.#offset;
181
+ }
182
+ isDate() {
183
+ return this.#hasDate && !this.#hasTime;
184
+ }
185
+ isTime() {
186
+ return this.#hasTime && !this.#hasDate;
187
+ }
188
+ isValid() {
189
+ return this.#hasDate || this.#hasTime;
190
+ }
191
+ toISOString() {
192
+ let iso = super.toISOString();
193
+ if (this.isDate())
194
+ return iso.slice(0, 10);
195
+ if (this.isTime())
196
+ return iso.slice(11, 23);
197
+ if (this.#offset === null)
198
+ return iso.slice(0, -1);
199
+ if (this.#offset === "Z")
200
+ return iso;
201
+ let offset = +this.#offset.slice(1, 3) * 60 + +this.#offset.slice(4, 6);
202
+ offset = this.#offset[0] === "-" ? offset : -offset;
203
+ let offsetDate = new Date(this.getTime() - offset * 6e4);
204
+ return offsetDate.toISOString().slice(0, -1) + this.#offset;
205
+ }
206
+ static wrapAsOffsetDateTime(jsDate, offset = "Z") {
207
+ let date = new _TomlDate(jsDate);
208
+ date.#offset = offset;
209
+ return date;
210
+ }
211
+ static wrapAsLocalDateTime(jsDate) {
212
+ let date = new _TomlDate(jsDate);
213
+ date.#offset = null;
214
+ return date;
215
+ }
216
+ static wrapAsLocalDate(jsDate) {
217
+ let date = new _TomlDate(jsDate);
218
+ date.#hasTime = false;
219
+ date.#offset = null;
220
+ return date;
221
+ }
222
+ static wrapAsLocalTime(jsDate) {
223
+ let date = new _TomlDate(jsDate);
224
+ date.#hasDate = false;
225
+ date.#offset = null;
226
+ return date;
227
+ }
228
+ };
229
+
230
+ // ../../node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/primitive.js
231
+ var INT_REGEX = /^((0x[0-9a-fA-F](_?[0-9a-fA-F])*)|(([+-]|0[ob])?\d(_?\d)*))$/;
232
+ var FLOAT_REGEX = /^[+-]?\d(_?\d)*(\.\d(_?\d)*)?([eE][+-]?\d(_?\d)*)?$/;
233
+ var LEADING_ZERO = /^[+-]?0[0-9_]/;
234
+ var ESCAPE_REGEX = /^[0-9a-f]{2,8}$/i;
235
+ var ESC_MAP = {
236
+ b: "\b",
237
+ t: " ",
238
+ n: "\n",
239
+ f: "\f",
240
+ r: "\r",
241
+ e: "\x1B",
242
+ '"': '"',
243
+ "\\": "\\"
244
+ };
245
+ function parseString(str, ptr = 0, endPtr = str.length) {
246
+ let isLiteral = str[ptr] === "'";
247
+ let isMultiline = str[ptr++] === str[ptr] && str[ptr] === str[ptr + 1];
248
+ if (isMultiline) {
249
+ endPtr -= 2;
250
+ if (str[ptr += 2] === "\r")
251
+ ptr++;
252
+ if (str[ptr] === "\n")
253
+ ptr++;
254
+ }
255
+ let tmp = 0;
256
+ let isEscape;
257
+ let parsed = "";
258
+ let sliceStart = ptr;
259
+ while (ptr < endPtr - 1) {
260
+ let c = str[ptr++];
261
+ if (c === "\n" || c === "\r" && str[ptr] === "\n") {
262
+ if (!isMultiline) {
263
+ throw new TomlError("newlines are not allowed in strings", {
264
+ toml: str,
265
+ ptr: ptr - 1
266
+ });
267
+ }
268
+ } else if (c < " " && c !== " " || c === "\x7F") {
269
+ throw new TomlError("control characters are not allowed in strings", {
270
+ toml: str,
271
+ ptr: ptr - 1
272
+ });
273
+ }
274
+ if (isEscape) {
275
+ isEscape = false;
276
+ if (c === "x" || c === "u" || c === "U") {
277
+ let code = str.slice(ptr, ptr += c === "x" ? 2 : c === "u" ? 4 : 8);
278
+ if (!ESCAPE_REGEX.test(code)) {
279
+ throw new TomlError("invalid unicode escape", {
280
+ toml: str,
281
+ ptr: tmp
282
+ });
283
+ }
284
+ try {
285
+ parsed += String.fromCodePoint(parseInt(code, 16));
286
+ } catch {
287
+ throw new TomlError("invalid unicode escape", {
288
+ toml: str,
289
+ ptr: tmp
290
+ });
291
+ }
292
+ } else if (isMultiline && (c === "\n" || c === " " || c === " " || c === "\r")) {
293
+ ptr = skipVoid(str, ptr - 1, true);
294
+ if (str[ptr] !== "\n" && str[ptr] !== "\r") {
295
+ throw new TomlError("invalid escape: only line-ending whitespace may be escaped", {
296
+ toml: str,
297
+ ptr: tmp
298
+ });
299
+ }
300
+ ptr = skipVoid(str, ptr);
301
+ } else if (c in ESC_MAP) {
302
+ parsed += ESC_MAP[c];
303
+ } else {
304
+ throw new TomlError("unrecognized escape sequence", {
305
+ toml: str,
306
+ ptr: tmp
307
+ });
308
+ }
309
+ sliceStart = ptr;
310
+ } else if (!isLiteral && c === "\\") {
311
+ tmp = ptr - 1;
312
+ isEscape = true;
313
+ parsed += str.slice(sliceStart, tmp);
314
+ }
315
+ }
316
+ return parsed + str.slice(sliceStart, endPtr - 1);
317
+ }
318
+ function parseValue(value, toml, ptr, integersAsBigInt) {
319
+ if (value === "true")
320
+ return true;
321
+ if (value === "false")
322
+ return false;
323
+ if (value === "-inf")
324
+ return -Infinity;
325
+ if (value === "inf" || value === "+inf")
326
+ return Infinity;
327
+ if (value === "nan" || value === "+nan" || value === "-nan")
328
+ return NaN;
329
+ if (value === "-0")
330
+ return integersAsBigInt ? 0n : 0;
331
+ let isInt = INT_REGEX.test(value);
332
+ if (isInt || FLOAT_REGEX.test(value)) {
333
+ if (LEADING_ZERO.test(value)) {
334
+ throw new TomlError("leading zeroes are not allowed", {
335
+ toml,
336
+ ptr
337
+ });
338
+ }
339
+ value = value.replace(/_/g, "");
340
+ let numeric = +value;
341
+ if (isNaN(numeric)) {
342
+ throw new TomlError("invalid number", {
343
+ toml,
344
+ ptr
345
+ });
346
+ }
347
+ if (isInt) {
348
+ if ((isInt = !Number.isSafeInteger(numeric)) && !integersAsBigInt) {
349
+ throw new TomlError("integer value cannot be represented losslessly", {
350
+ toml,
351
+ ptr
352
+ });
353
+ }
354
+ if (isInt || integersAsBigInt === true)
355
+ numeric = BigInt(value);
356
+ }
357
+ return numeric;
358
+ }
359
+ const date = new TomlDate(value);
360
+ if (!date.isValid()) {
361
+ throw new TomlError("invalid value", {
362
+ toml,
363
+ ptr
364
+ });
365
+ }
366
+ return date;
367
+ }
368
+
369
+ // ../../node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/extract.js
370
+ function sliceAndTrimEndOf(str, startPtr, endPtr) {
371
+ let value = str.slice(startPtr, endPtr);
372
+ let commentIdx = value.indexOf("#");
373
+ if (commentIdx > -1) {
374
+ skipComment(str, commentIdx);
375
+ value = value.slice(0, commentIdx);
376
+ }
377
+ return [value.trimEnd(), commentIdx];
378
+ }
379
+ function extractValue(str, ptr, end, depth, integersAsBigInt) {
380
+ if (depth === 0) {
381
+ throw new TomlError("document contains excessively nested structures. aborting.", {
382
+ toml: str,
383
+ ptr
384
+ });
385
+ }
386
+ let c = str[ptr];
387
+ if (c === "[" || c === "{") {
388
+ let [value, endPtr2] = c === "[" ? parseArray(str, ptr, depth, integersAsBigInt) : parseInlineTable(str, ptr, depth, integersAsBigInt);
389
+ if (end) {
390
+ endPtr2 = skipVoid(str, endPtr2);
391
+ if (str[endPtr2] === ",")
392
+ endPtr2++;
393
+ else if (str[endPtr2] !== end) {
394
+ throw new TomlError("expected comma or end of structure", {
395
+ toml: str,
396
+ ptr: endPtr2
397
+ });
398
+ }
399
+ }
400
+ return [value, endPtr2];
401
+ }
402
+ let endPtr;
403
+ if (c === '"' || c === "'") {
404
+ endPtr = getStringEnd(str, ptr);
405
+ let parsed = parseString(str, ptr, endPtr);
406
+ if (end) {
407
+ endPtr = skipVoid(str, endPtr);
408
+ if (str[endPtr] && str[endPtr] !== "," && str[endPtr] !== end && str[endPtr] !== "\n" && str[endPtr] !== "\r") {
409
+ throw new TomlError("unexpected character encountered", {
410
+ toml: str,
411
+ ptr: endPtr
412
+ });
413
+ }
414
+ endPtr += +(str[endPtr] === ",");
415
+ }
416
+ return [parsed, endPtr];
417
+ }
418
+ endPtr = skipUntil(str, ptr, ",", end);
419
+ let slice = sliceAndTrimEndOf(str, ptr, endPtr - +(str[endPtr - 1] === ","));
420
+ if (!slice[0]) {
421
+ throw new TomlError("incomplete key-value declaration: no value specified", {
422
+ toml: str,
423
+ ptr
424
+ });
425
+ }
426
+ if (end && slice[1] > -1) {
427
+ endPtr = skipVoid(str, ptr + slice[1]);
428
+ endPtr += +(str[endPtr] === ",");
429
+ }
430
+ return [
431
+ parseValue(slice[0], str, ptr, integersAsBigInt),
432
+ endPtr
433
+ ];
434
+ }
435
+
436
+ // ../../node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/struct.js
437
+ var KEY_PART_RE = /^[a-zA-Z0-9-_]+[ \t]*$/;
438
+ function parseKey(str, ptr, end = "=") {
439
+ let dot = ptr - 1;
440
+ let parsed = [];
441
+ let endPtr = str.indexOf(end, ptr);
442
+ if (endPtr < 0) {
443
+ throw new TomlError("incomplete key-value: cannot find end of key", {
444
+ toml: str,
445
+ ptr
446
+ });
447
+ }
448
+ do {
449
+ let c = str[ptr = ++dot];
450
+ if (c !== " " && c !== " ") {
451
+ if (c === '"' || c === "'") {
452
+ if (c === str[ptr + 1] && c === str[ptr + 2]) {
453
+ throw new TomlError("multiline strings are not allowed in keys", {
454
+ toml: str,
455
+ ptr
456
+ });
457
+ }
458
+ let eos = getStringEnd(str, ptr);
459
+ if (eos < 0) {
460
+ throw new TomlError("unfinished string encountered", {
461
+ toml: str,
462
+ ptr
463
+ });
464
+ }
465
+ dot = str.indexOf(".", eos);
466
+ let strEnd = str.slice(eos, dot < 0 || dot > endPtr ? endPtr : dot);
467
+ let newLine = indexOfNewline(strEnd);
468
+ if (newLine > -1) {
469
+ throw new TomlError("newlines are not allowed in keys", {
470
+ toml: str,
471
+ ptr: ptr + dot + newLine
472
+ });
473
+ }
474
+ if (strEnd.trimStart()) {
475
+ throw new TomlError("found extra tokens after the string part", {
476
+ toml: str,
477
+ ptr: eos
478
+ });
479
+ }
480
+ if (endPtr < eos) {
481
+ endPtr = str.indexOf(end, eos);
482
+ if (endPtr < 0) {
483
+ throw new TomlError("incomplete key-value: cannot find end of key", {
484
+ toml: str,
485
+ ptr
486
+ });
487
+ }
488
+ }
489
+ parsed.push(parseString(str, ptr, eos));
490
+ } else {
491
+ dot = str.indexOf(".", ptr);
492
+ let part = str.slice(ptr, dot < 0 || dot > endPtr ? endPtr : dot);
493
+ if (!KEY_PART_RE.test(part)) {
494
+ throw new TomlError("only letter, numbers, dashes and underscores are allowed in keys", {
495
+ toml: str,
496
+ ptr
497
+ });
498
+ }
499
+ parsed.push(part.trimEnd());
500
+ }
501
+ }
502
+ } while (dot + 1 && dot < endPtr);
503
+ return [parsed, skipVoid(str, endPtr + 1, true, true)];
504
+ }
505
+ function parseInlineTable(str, ptr, depth, integersAsBigInt) {
506
+ let res = {};
507
+ let seen = /* @__PURE__ */ new Set();
508
+ let c;
509
+ ptr++;
510
+ while ((c = str[ptr++]) !== "}" && c) {
511
+ if (c === ",") {
512
+ throw new TomlError("expected value, found comma", {
513
+ toml: str,
514
+ ptr: ptr - 1
515
+ });
516
+ } else if (c === "#")
517
+ ptr = skipComment(str, ptr);
518
+ else if (c !== " " && c !== " " && c !== "\n" && c !== "\r") {
519
+ let k;
520
+ let t = res;
521
+ let hasOwn = false;
522
+ let [key, keyEndPtr] = parseKey(str, ptr - 1);
523
+ for (let i = 0; i < key.length; i++) {
524
+ if (i)
525
+ t = hasOwn ? t[k] : t[k] = {};
526
+ k = key[i];
527
+ if ((hasOwn = Object.hasOwn(t, k)) && (typeof t[k] !== "object" || seen.has(t[k]))) {
528
+ throw new TomlError("trying to redefine an already defined value", {
529
+ toml: str,
530
+ ptr
531
+ });
532
+ }
533
+ if (!hasOwn && k === "__proto__") {
534
+ Object.defineProperty(t, k, { enumerable: true, configurable: true, writable: true });
535
+ }
536
+ }
537
+ if (hasOwn) {
538
+ throw new TomlError("trying to redefine an already defined value", {
539
+ toml: str,
540
+ ptr
541
+ });
542
+ }
543
+ let [value, valueEndPtr] = extractValue(str, keyEndPtr, "}", depth - 1, integersAsBigInt);
544
+ seen.add(value);
545
+ t[k] = value;
546
+ ptr = valueEndPtr;
547
+ }
548
+ }
549
+ if (!c) {
550
+ throw new TomlError("unfinished table encountered", {
551
+ toml: str,
552
+ ptr
553
+ });
554
+ }
555
+ return [res, ptr];
556
+ }
557
+ function parseArray(str, ptr, depth, integersAsBigInt) {
558
+ let res = [];
559
+ let c;
560
+ ptr++;
561
+ while ((c = str[ptr++]) !== "]" && c) {
562
+ if (c === ",") {
563
+ throw new TomlError("expected value, found comma", {
564
+ toml: str,
565
+ ptr: ptr - 1
566
+ });
567
+ } else if (c === "#")
568
+ ptr = skipComment(str, ptr);
569
+ else if (c !== " " && c !== " " && c !== "\n" && c !== "\r") {
570
+ let e = extractValue(str, ptr - 1, "]", depth - 1, integersAsBigInt);
571
+ res.push(e[0]);
572
+ ptr = e[1];
573
+ }
574
+ }
575
+ if (!c) {
576
+ throw new TomlError("unfinished array encountered", {
577
+ toml: str,
578
+ ptr
579
+ });
580
+ }
581
+ return [res, ptr];
582
+ }
583
+
584
+ // ../../node_modules/.pnpm/smol-toml@1.6.1/node_modules/smol-toml/dist/parse.js
585
+ function peekTable(key, table, meta, type) {
586
+ let t = table;
587
+ let m = meta;
588
+ let k;
589
+ let hasOwn = false;
590
+ let state;
591
+ for (let i = 0; i < key.length; i++) {
592
+ if (i) {
593
+ t = hasOwn ? t[k] : t[k] = {};
594
+ m = (state = m[k]).c;
595
+ if (type === 0 && (state.t === 1 || state.t === 2)) {
596
+ return null;
597
+ }
598
+ if (state.t === 2) {
599
+ let l = t.length - 1;
600
+ t = t[l];
601
+ m = m[l].c;
602
+ }
603
+ }
604
+ k = key[i];
605
+ if ((hasOwn = Object.hasOwn(t, k)) && m[k]?.t === 0 && m[k]?.d) {
606
+ return null;
607
+ }
608
+ if (!hasOwn) {
609
+ if (k === "__proto__") {
610
+ Object.defineProperty(t, k, { enumerable: true, configurable: true, writable: true });
611
+ Object.defineProperty(m, k, { enumerable: true, configurable: true, writable: true });
612
+ }
613
+ m[k] = {
614
+ t: i < key.length - 1 && type === 2 ? 3 : type,
615
+ d: false,
616
+ i: 0,
617
+ c: {}
618
+ };
619
+ }
620
+ }
621
+ state = m[k];
622
+ if (state.t !== type && !(type === 1 && state.t === 3)) {
623
+ return null;
624
+ }
625
+ if (type === 2) {
626
+ if (!state.d) {
627
+ state.d = true;
628
+ t[k] = [];
629
+ }
630
+ t[k].push(t = {});
631
+ state.c[state.i++] = state = { t: 1, d: false, i: 0, c: {} };
632
+ }
633
+ if (state.d) {
634
+ return null;
635
+ }
636
+ state.d = true;
637
+ if (type === 1) {
638
+ t = hasOwn ? t[k] : t[k] = {};
639
+ } else if (type === 0 && hasOwn) {
640
+ return null;
641
+ }
642
+ return [k, t, state.c];
643
+ }
644
+ function parse(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
645
+ let res = {};
646
+ let meta = {};
647
+ let tbl = res;
648
+ let m = meta;
649
+ for (let ptr = skipVoid(toml, 0); ptr < toml.length; ) {
650
+ if (toml[ptr] === "[") {
651
+ let isTableArray = toml[++ptr] === "[";
652
+ let k = parseKey(toml, ptr += +isTableArray, "]");
653
+ if (isTableArray) {
654
+ if (toml[k[1] - 1] !== "]") {
655
+ throw new TomlError("expected end of table declaration", {
656
+ toml,
657
+ ptr: k[1] - 1
658
+ });
659
+ }
660
+ k[1]++;
661
+ }
662
+ let p = peekTable(
663
+ k[0],
664
+ res,
665
+ meta,
666
+ isTableArray ? 2 : 1
667
+ /* Type.EXPLICIT */
668
+ );
669
+ if (!p) {
670
+ throw new TomlError("trying to redefine an already defined table or value", {
671
+ toml,
672
+ ptr
673
+ });
674
+ }
675
+ m = p[2];
676
+ tbl = p[1];
677
+ ptr = k[1];
678
+ } else {
679
+ let k = parseKey(toml, ptr);
680
+ let p = peekTable(
681
+ k[0],
682
+ tbl,
683
+ m,
684
+ 0
685
+ /* Type.DOTTED */
686
+ );
687
+ if (!p) {
688
+ throw new TomlError("trying to redefine an already defined table or value", {
689
+ toml,
690
+ ptr
691
+ });
692
+ }
693
+ let v = extractValue(toml, k[1], void 0, maxDepth, integersAsBigInt);
694
+ p[1][p[0]] = v[0];
695
+ ptr = v[1];
696
+ }
697
+ ptr = skipVoid(toml, ptr, true);
698
+ if (toml[ptr] && toml[ptr] !== "\n" && toml[ptr] !== "\r") {
699
+ throw new TomlError("each key-value declaration must be followed by an end-of-line", {
700
+ toml,
701
+ ptr
702
+ });
703
+ }
704
+ ptr = skipVoid(toml, ptr);
705
+ }
706
+ return res;
707
+ }
708
+
709
+ // ../core/dist/index.js
710
+ import { mkdirSync as mkdirSync2, appendFileSync } from "fs";
711
+ import { homedir as homedir3 } from "os";
712
+ import { join as join3 } from "path";
713
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
714
+ import { homedir as homedir4 } from "os";
715
+ import { join as join4 } from "path";
716
+ import * as fs2 from "fs";
717
+ import * as path2 from "path";
718
+ import * as os2 from "os";
719
+ import { execFileSync } from "child_process";
720
+ function base64url(input) {
721
+ const buf = typeof input === "string" ? Buffer.from(input) : input;
722
+ return buf.toString("base64url");
723
+ }
724
+ function hashQuery(queryString) {
725
+ return createHash("sha512").update(queryString, "utf8").digest("hex");
726
+ }
727
+ function createJwt(payload, secretKey) {
728
+ const header = { alg: "HS256", typ: "JWT" };
729
+ const headerB64 = base64url(JSON.stringify(header));
730
+ const payloadB64 = base64url(JSON.stringify(payload));
731
+ const signature = createHmac("sha256", secretKey).update(`${headerB64}.${payloadB64}`).digest("base64url");
732
+ return `${headerB64}.${payloadB64}.${signature}`;
733
+ }
734
+ function signRequest(accessKey, secretKey, queryString) {
735
+ const payload = {
736
+ access_key: accessKey,
737
+ nonce: randomUUID(),
738
+ timestamp: Date.now()
739
+ };
740
+ if (queryString && queryString.length > 0) {
741
+ payload.query_hash = hashQuery(queryString);
742
+ payload.query_hash_alg = "SHA512";
743
+ }
744
+ return createJwt(payload, secretKey);
745
+ }
746
+ var BithumbMcpError = class extends Error {
747
+ type;
748
+ code;
749
+ suggestion;
750
+ endpoint;
751
+ constructor(type, message, options) {
752
+ super(message, options?.cause ? { cause: options.cause } : void 0);
753
+ this.name = type;
754
+ this.type = type;
755
+ this.code = options?.code;
756
+ this.suggestion = options?.suggestion;
757
+ this.endpoint = options?.endpoint;
758
+ }
759
+ };
760
+ var ConfigError = class extends BithumbMcpError {
761
+ constructor(message, suggestion) {
762
+ super("ConfigError", message, { suggestion });
763
+ }
764
+ };
765
+ var ValidationError = class extends BithumbMcpError {
766
+ constructor(message, suggestion) {
767
+ super("ValidationError", message, { suggestion });
768
+ }
769
+ };
770
+ var RateLimitError = class extends BithumbMcpError {
771
+ constructor(message, suggestion, endpoint) {
772
+ super("RateLimitError", message, { suggestion, endpoint });
773
+ }
774
+ };
775
+ var AuthenticationError = class extends BithumbMcpError {
776
+ constructor(message, suggestion, endpoint) {
777
+ super("AuthenticationError", message, { suggestion, endpoint });
778
+ }
779
+ };
780
+ var BithumbApiError = class extends BithumbMcpError {
781
+ constructor(message, options) {
782
+ super("BithumbApiError", message, options);
783
+ }
784
+ };
785
+ var NetworkError = class extends BithumbMcpError {
786
+ constructor(message, endpoint, cause) {
787
+ super("NetworkError", message, {
788
+ endpoint,
789
+ cause,
790
+ suggestion: "Check network connectivity and try again."
791
+ });
792
+ }
793
+ };
794
+ function toToolErrorPayload(error) {
795
+ if (error instanceof BithumbMcpError) {
796
+ return {
797
+ error: true,
798
+ type: error.type,
799
+ code: error.code,
800
+ message: error.message,
801
+ suggestion: error.suggestion,
802
+ endpoint: error.endpoint,
803
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
804
+ };
805
+ }
806
+ const message = error instanceof Error ? error.message : "An unexpected error occurred.";
807
+ return {
808
+ error: true,
809
+ type: "InternalError",
810
+ message,
811
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
812
+ };
813
+ }
814
+ var RateLimiter = class {
815
+ buckets = /* @__PURE__ */ new Map();
816
+ cleanupMs;
817
+ verbose;
818
+ lastCleanup;
819
+ constructor(cleanupMs = 3e4, verbose = false) {
820
+ this.cleanupMs = cleanupMs;
821
+ this.verbose = verbose;
822
+ this.lastCleanup = Date.now();
823
+ }
824
+ async consume(config, amount = 1) {
825
+ this.maybeCleanup();
826
+ const bucket = this.getBucket(config);
827
+ this.refill(bucket);
828
+ if (bucket.tokens >= amount) {
829
+ bucket.tokens -= amount;
830
+ return;
831
+ }
832
+ const deficit = amount - bucket.tokens;
833
+ const waitMs = Math.ceil(deficit / bucket.refillPerSecond * 1e3);
834
+ if (waitMs > 1e4) {
835
+ throw new RateLimitError(
836
+ `Rate limit exceeded for "${config.key}". Need ${deficit.toFixed(1)} tokens, wait ~${waitMs}ms.`,
837
+ "Reduce request frequency or wait."
838
+ );
839
+ }
840
+ if (this.verbose) {
841
+ process.stderr.write(
842
+ `[rate-limiter] ${config.key}: waiting ${waitMs}ms for ${deficit.toFixed(1)} tokens
843
+ `
844
+ );
845
+ }
846
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
847
+ this.refill(bucket);
848
+ if (bucket.tokens >= amount) {
849
+ bucket.tokens -= amount;
850
+ return;
851
+ }
852
+ throw new RateLimitError(
853
+ `Rate limit exceeded for "${config.key}" after waiting.`,
854
+ "Reduce request frequency."
855
+ );
856
+ }
857
+ getBucket(config) {
858
+ let bucket = this.buckets.get(config.key);
859
+ if (!bucket) {
860
+ bucket = {
861
+ tokens: config.capacity,
862
+ capacity: config.capacity,
863
+ refillPerSecond: config.refillPerSecond,
864
+ lastRefill: Date.now()
865
+ };
866
+ this.buckets.set(config.key, bucket);
867
+ }
868
+ return bucket;
869
+ }
870
+ refill(bucket) {
871
+ const now = Date.now();
872
+ const elapsed = (now - bucket.lastRefill) / 1e3;
873
+ if (elapsed <= 0) return;
874
+ bucket.tokens = Math.min(
875
+ bucket.capacity,
876
+ bucket.tokens + elapsed * bucket.refillPerSecond
877
+ );
878
+ bucket.lastRefill = now;
879
+ }
880
+ maybeCleanup() {
881
+ const now = Date.now();
882
+ if (now - this.lastCleanup < this.cleanupMs) return;
883
+ this.lastCleanup = now;
884
+ for (const [key, bucket] of this.buckets) {
885
+ const idle = (now - bucket.lastRefill) / 1e3;
886
+ if (idle > 60 && bucket.tokens >= bucket.capacity) {
887
+ this.buckets.delete(key);
888
+ }
889
+ }
890
+ }
891
+ };
892
+ var BITHUMB_API_BASE_URL = "https://api.bithumb.com";
893
+ var MODULES = ["market", "account", "trade", "twap", "withdraw", "deposit", "system"];
894
+ var DEFAULT_MODULES = ["market", "account", "trade", "twap", "withdraw", "deposit", "system"];
895
+ var TRADE_KIT_HEADER = "X-AI-Trade-Kit";
896
+ function isDefined(value) {
897
+ return value !== void 0 && value !== null;
898
+ }
899
+ function buildQueryString(query) {
900
+ if (!query) return "";
901
+ const entries = Object.entries(query).filter(([, v]) => isDefined(v));
902
+ if (entries.length === 0) return "";
903
+ const parts = [];
904
+ for (const [key, value] of entries) {
905
+ if (Array.isArray(value)) {
906
+ for (let i = 0; i < value.length; i++) {
907
+ const item = value[i];
908
+ if (item && typeof item === "object" && !Array.isArray(item)) {
909
+ for (const [subKey, subVal] of Object.entries(item)) {
910
+ parts.push(
911
+ `${encodeURIComponent(key)}[${i}][${encodeURIComponent(subKey)}]=${encodeURIComponent(String(subVal))}`
912
+ );
913
+ }
914
+ } else {
915
+ parts.push(
916
+ `${encodeURIComponent(key)}[]=${encodeURIComponent(String(item))}`
917
+ );
918
+ }
919
+ }
920
+ } else {
921
+ parts.push(
922
+ `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`
923
+ );
924
+ }
925
+ }
926
+ return parts.join("&");
927
+ }
928
+ function buildHashString(query, body) {
929
+ const source = body ?? query;
930
+ if (!source) return "";
931
+ const entries = Object.entries(source).filter(([, v]) => isDefined(v));
932
+ if (entries.length === 0) return "";
933
+ const parts = [];
934
+ for (const [key, value] of entries) {
935
+ if (Array.isArray(value)) {
936
+ for (let i = 0; i < value.length; i++) {
937
+ const item = value[i];
938
+ if (item && typeof item === "object" && !Array.isArray(item)) {
939
+ for (const [subKey, subVal] of Object.entries(item)) {
940
+ parts.push(`${key}[${i}][${subKey}]=${String(subVal)}`);
941
+ }
942
+ } else {
943
+ parts.push(`${key}[]=${String(item)}`);
944
+ }
945
+ }
946
+ } else {
947
+ parts.push(`${key}=${String(value)}`);
948
+ }
949
+ }
950
+ return parts.join("&");
951
+ }
952
+ function vlog(message) {
953
+ process.stderr.write(`[verbose] ${message}
954
+ `);
955
+ }
956
+ var BithumbRestClient = class {
957
+ config;
958
+ rateLimiter;
959
+ constructor(config) {
960
+ this.config = config;
961
+ this.rateLimiter = new RateLimiter(3e4, config.verbose);
962
+ }
963
+ async publicGet(path3, query, rateLimit) {
964
+ return this.request({
965
+ method: "GET",
966
+ path: path3,
967
+ auth: "public",
968
+ query,
969
+ rateLimit
970
+ });
971
+ }
972
+ async privateGet(path3, query, rateLimit) {
973
+ return this.request({
974
+ method: "GET",
975
+ path: path3,
976
+ auth: "private",
977
+ query,
978
+ rateLimit
979
+ });
980
+ }
981
+ async privatePost(path3, body, rateLimit) {
982
+ return this.request({
983
+ method: "POST",
984
+ path: path3,
985
+ auth: "private",
986
+ body,
987
+ rateLimit
988
+ });
989
+ }
990
+ async privateDelete(path3, query, rateLimit) {
991
+ return this.request({
992
+ method: "DELETE",
993
+ path: path3,
994
+ auth: "private",
995
+ query,
996
+ rateLimit
997
+ });
998
+ }
999
+ // ─── Core request ────────────────────────────────────────────────
1000
+ async request(reqConfig) {
1001
+ const queryString = buildQueryString(reqConfig.query);
1002
+ const requestPath = queryString.length > 0 ? `${reqConfig.path}?${queryString}` : reqConfig.path;
1003
+ const url = `${this.config.baseUrl}${requestPath}`;
1004
+ if (this.config.verbose) vlog(`\u2192 ${reqConfig.method} ${url}`);
1005
+ if (reqConfig.rateLimit) await this.rateLimiter.consume(reqConfig.rateLimit);
1006
+ const headers = {
1007
+ "Content-Type": "application/json",
1008
+ Accept: "application/json",
1009
+ [TRADE_KIT_HEADER]: this.config.clientType
1010
+ };
1011
+ if (reqConfig.auth === "private") {
1012
+ if (!this.config.hasAuth || !this.config.accessKey || !this.config.secretKey) {
1013
+ throw new ConfigError(
1014
+ "Private endpoint requires API credentials.",
1015
+ "Set BITHUMB_ACCESS_KEY and BITHUMB_SECRET_KEY."
1016
+ );
1017
+ }
1018
+ const hashStr = buildHashString(reqConfig.query, reqConfig.body);
1019
+ const token = signRequest(
1020
+ this.config.accessKey,
1021
+ this.config.secretKey,
1022
+ hashStr
1023
+ );
1024
+ headers["Authorization"] = `Bearer ${token}`;
1025
+ }
1026
+ const t0 = Date.now();
1027
+ let response;
1028
+ try {
1029
+ const fetchOptions = {
1030
+ method: reqConfig.method,
1031
+ headers,
1032
+ signal: AbortSignal.timeout(this.config.timeoutMs)
1033
+ };
1034
+ if (reqConfig.body && reqConfig.method === "POST") {
1035
+ fetchOptions.body = JSON.stringify(reqConfig.body);
1036
+ }
1037
+ response = await fetch(url, fetchOptions);
1038
+ } catch (error) {
1039
+ if (this.config.verbose) {
1040
+ vlog(`\u2717 NetworkError after ${Date.now() - t0}ms`);
1041
+ }
1042
+ throw new NetworkError(
1043
+ `Failed to call ${reqConfig.method} ${reqConfig.path}.`,
1044
+ `${reqConfig.method} ${reqConfig.path}`,
1045
+ error
1046
+ );
1047
+ }
1048
+ const rawText = await response.text();
1049
+ const elapsed = Date.now() - t0;
1050
+ if (this.config.verbose) {
1051
+ vlog(`\u2190 ${response.status} | ${rawText.length}B | ${elapsed}ms`);
1052
+ }
1053
+ let parsed;
1054
+ try {
1055
+ parsed = rawText ? JSON.parse(rawText) : null;
1056
+ } catch {
1057
+ throw new NetworkError(
1058
+ `Non-JSON response from ${reqConfig.method} ${reqConfig.path}.`,
1059
+ `${reqConfig.method} ${reqConfig.path}`
1060
+ );
1061
+ }
1062
+ if (parsed && typeof parsed === "object" && "error" in parsed) {
1063
+ const errResp = parsed;
1064
+ const errName = errResp.error?.name;
1065
+ const errMsg = errResp.error?.message;
1066
+ const displayMsg = [errName, errMsg].filter(Boolean).join(": ") || `HTTP ${response.status}`;
1067
+ const endpoint = `${reqConfig.method} ${reqConfig.path}`;
1068
+ if (response.status === 401) {
1069
+ throw new AuthenticationError(
1070
+ displayMsg,
1071
+ "Check BITHUMB_ACCESS_KEY and BITHUMB_SECRET_KEY.",
1072
+ endpoint
1073
+ );
1074
+ }
1075
+ if (response.status === 429) {
1076
+ throw new RateLimitError(
1077
+ displayMsg,
1078
+ "Reduce request frequency.",
1079
+ endpoint
1080
+ );
1081
+ }
1082
+ throw new BithumbApiError(displayMsg, {
1083
+ code: errName ?? String(response.status),
1084
+ endpoint
1085
+ });
1086
+ }
1087
+ if (!response.ok) {
1088
+ throw new BithumbApiError(`HTTP ${response.status}`, {
1089
+ code: String(response.status),
1090
+ endpoint: `${reqConfig.method} ${reqConfig.path}`
1091
+ });
1092
+ }
1093
+ return {
1094
+ endpoint: `${reqConfig.method} ${reqConfig.path}`,
1095
+ requestTime: (/* @__PURE__ */ new Date()).toISOString(),
1096
+ data: parsed
1097
+ };
1098
+ }
1099
+ };
1100
+ function asRecord(args) {
1101
+ if (args && typeof args === "object" && !Array.isArray(args)) {
1102
+ return args;
1103
+ }
1104
+ return {};
1105
+ }
1106
+ function readString(args, key) {
1107
+ const v = args[key];
1108
+ if (v === void 0 || v === null || v === "") return void 0;
1109
+ return String(v);
1110
+ }
1111
+ function readNumber(args, key) {
1112
+ const v = args[key];
1113
+ if (v === void 0 || v === null || v === "") return void 0;
1114
+ const n = Number(v);
1115
+ if (!Number.isFinite(n)) return void 0;
1116
+ return n;
1117
+ }
1118
+ function readBoolean(args, key) {
1119
+ const v = args[key];
1120
+ if (v === void 0 || v === null) return void 0;
1121
+ if (typeof v === "boolean") return v;
1122
+ if (v === "true" || v === "1") return true;
1123
+ if (v === "false" || v === "0") return false;
1124
+ return void 0;
1125
+ }
1126
+ function readStringArray(args, key) {
1127
+ const v = args[key];
1128
+ if (v === void 0 || v === null) return void 0;
1129
+ if (Array.isArray(v)) return v.map(String);
1130
+ if (typeof v === "string") {
1131
+ return v.split(",").map((s) => s.trim()).filter(Boolean);
1132
+ }
1133
+ return void 0;
1134
+ }
1135
+ function requireString(args, key) {
1136
+ const v = readString(args, key);
1137
+ if (!v) {
1138
+ throw new ValidationError(
1139
+ `Missing required parameter "${key}".`,
1140
+ `Provide a non-empty "${key}" string.`
1141
+ );
1142
+ }
1143
+ return v;
1144
+ }
1145
+ function compactObject(obj) {
1146
+ const result = {};
1147
+ for (const [key, value] of Object.entries(obj)) {
1148
+ if (value !== void 0 && value !== null) {
1149
+ result[key] = value;
1150
+ }
1151
+ }
1152
+ return result;
1153
+ }
1154
+ function normalizeResponse(data) {
1155
+ if (data === null || data === void 0) return {};
1156
+ return data;
1157
+ }
1158
+ function publicRateLimit(key, rps = 150) {
1159
+ return { key: `public:${key}`, capacity: rps, refillPerSecond: rps };
1160
+ }
1161
+ function privateRateLimit(key, rps = 140) {
1162
+ return { key: `private:${key}`, capacity: rps, refillPerSecond: rps };
1163
+ }
1164
+ function orderRateLimit(key, rps = 10) {
1165
+ return { key: `order:${key}`, capacity: rps, refillPerSecond: rps };
1166
+ }
1167
+ function registerMarketTools() {
1168
+ return [
1169
+ // ── 1. market_get_markets ──────────────────────────────────────
1170
+ {
1171
+ name: "market_get_markets",
1172
+ module: "market",
1173
+ description: "\uBE57\uC378 \uAC70\uB798 \uAC00\uB2A5\uD55C \uB9C8\uCF13(\uAC70\uB798 \uD398\uC5B4) \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. Get all available trading pairs on Bithumb.",
1174
+ isWrite: false,
1175
+ inputSchema: {
1176
+ type: "object",
1177
+ properties: {
1178
+ isDetails: {
1179
+ type: "boolean",
1180
+ description: "Include detailed market info"
1181
+ }
1182
+ },
1183
+ required: []
1184
+ },
1185
+ handler: async (rawArgs, context) => {
1186
+ const args = asRecord(rawArgs);
1187
+ const response = await context.client.publicGet(
1188
+ "/v1/market/all",
1189
+ compactObject({ isDetails: readBoolean(args, "isDetails") }),
1190
+ publicRateLimit("market_get_markets")
1191
+ );
1192
+ return normalizeResponse(response);
1193
+ }
1194
+ },
1195
+ // ── 2. market_get_ticker ───────────────────────────────────────
1196
+ {
1197
+ name: "market_get_ticker",
1198
+ module: "market",
1199
+ description: "\uD604\uC7AC\uAC00(Ticker) \uC815\uBCF4\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. Get current price ticker for specified markets.",
1200
+ isWrite: false,
1201
+ inputSchema: {
1202
+ type: "object",
1203
+ properties: {
1204
+ markets: {
1205
+ type: "string",
1206
+ description: "Comma-separated market codes, e.g. KRW-BTC,KRW-ETH"
1207
+ }
1208
+ },
1209
+ required: ["markets"]
1210
+ },
1211
+ handler: async (rawArgs, context) => {
1212
+ const args = asRecord(rawArgs);
1213
+ const response = await context.client.publicGet(
1214
+ "/v1/ticker",
1215
+ compactObject({ markets: requireString(args, "markets") }),
1216
+ publicRateLimit("market_get_ticker")
1217
+ );
1218
+ return normalizeResponse(response);
1219
+ }
1220
+ },
1221
+ // ── 3. market_get_orderbook ────────────────────────────────────
1222
+ {
1223
+ name: "market_get_orderbook",
1224
+ module: "market",
1225
+ description: "\uD638\uAC00(Orderbook) \uC815\uBCF4\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. Get orderbook (bids/asks) for specified markets.",
1226
+ isWrite: false,
1227
+ inputSchema: {
1228
+ type: "object",
1229
+ properties: {
1230
+ markets: {
1231
+ type: "string",
1232
+ description: "Comma-separated market codes, e.g. KRW-BTC,KRW-ETH"
1233
+ }
1234
+ },
1235
+ required: ["markets"]
1236
+ },
1237
+ handler: async (rawArgs, context) => {
1238
+ const args = asRecord(rawArgs);
1239
+ const response = await context.client.publicGet(
1240
+ "/v1/orderbook",
1241
+ compactObject({ markets: requireString(args, "markets") }),
1242
+ publicRateLimit("market_get_orderbook")
1243
+ );
1244
+ return normalizeResponse(response);
1245
+ }
1246
+ },
1247
+ // ── 4. market_get_trades ───────────────────────────────────────
1248
+ {
1249
+ name: "market_get_trades",
1250
+ module: "market",
1251
+ description: "\uCD5C\uADFC \uCCB4\uACB0 \uB0B4\uC5ED\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. Get recent trades for a market.",
1252
+ isWrite: false,
1253
+ inputSchema: {
1254
+ type: "object",
1255
+ properties: {
1256
+ market: {
1257
+ type: "string",
1258
+ description: "Market code, e.g. KRW-BTC"
1259
+ },
1260
+ to: {
1261
+ type: "string",
1262
+ description: "Return trades before this timestamp (exclusive)"
1263
+ },
1264
+ count: {
1265
+ type: "number",
1266
+ description: "Number of trades to return (1-500)"
1267
+ },
1268
+ cursor: {
1269
+ type: "string",
1270
+ description: "Pagination cursor from previous response"
1271
+ },
1272
+ daysAgo: {
1273
+ type: "number",
1274
+ description: "Filter trades from N days ago (1-7)"
1275
+ }
1276
+ },
1277
+ required: ["market"]
1278
+ },
1279
+ handler: async (rawArgs, context) => {
1280
+ const args = asRecord(rawArgs);
1281
+ const response = await context.client.publicGet(
1282
+ "/v1/trades/ticks",
1283
+ compactObject({
1284
+ market: requireString(args, "market"),
1285
+ to: readString(args, "to"),
1286
+ count: readNumber(args, "count"),
1287
+ cursor: readString(args, "cursor"),
1288
+ daysAgo: readNumber(args, "daysAgo")
1289
+ }),
1290
+ publicRateLimit("market_get_trades")
1291
+ );
1292
+ return normalizeResponse(response);
1293
+ }
1294
+ },
1295
+ // ── 5. market_get_candles_minutes ──────────────────────────────
1296
+ {
1297
+ name: "market_get_candles_minutes",
1298
+ module: "market",
1299
+ description: "\uBD84(minute) \uCE94\uB4E4\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. Get minute candles (OHLCV).",
1300
+ isWrite: false,
1301
+ inputSchema: {
1302
+ type: "object",
1303
+ properties: {
1304
+ unit: {
1305
+ type: "number",
1306
+ enum: [1, 3, 5, 10, 15, 30, 60, 240],
1307
+ description: "Candle unit in minutes"
1308
+ },
1309
+ market: {
1310
+ type: "string",
1311
+ description: "Market code, e.g. KRW-BTC"
1312
+ },
1313
+ to: {
1314
+ type: "string",
1315
+ description: "Return candles before this timestamp"
1316
+ },
1317
+ count: {
1318
+ type: "number",
1319
+ description: "Number of candles to return (max 200)"
1320
+ }
1321
+ },
1322
+ required: ["unit", "market"]
1323
+ },
1324
+ handler: async (rawArgs, context) => {
1325
+ const args = asRecord(rawArgs);
1326
+ const unit = readNumber(args, "unit");
1327
+ if (!unit) throw new Error('Missing required parameter "unit".');
1328
+ const response = await context.client.publicGet(
1329
+ `/v1/candles/minutes/${unit}`,
1330
+ compactObject({
1331
+ market: requireString(args, "market"),
1332
+ to: readString(args, "to"),
1333
+ count: readNumber(args, "count")
1334
+ }),
1335
+ publicRateLimit("market_get_candles_minutes")
1336
+ );
1337
+ return normalizeResponse(response);
1338
+ }
1339
+ },
1340
+ // ── 6. market_get_candles_days ─────────────────────────────────
1341
+ {
1342
+ name: "market_get_candles_days",
1343
+ module: "market",
1344
+ description: "\uC77C(day) \uCE94\uB4E4\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. Get daily candles.",
1345
+ isWrite: false,
1346
+ inputSchema: {
1347
+ type: "object",
1348
+ properties: {
1349
+ market: {
1350
+ type: "string",
1351
+ description: "Market code, e.g. KRW-BTC"
1352
+ },
1353
+ to: {
1354
+ type: "string",
1355
+ description: "Return candles before this timestamp"
1356
+ },
1357
+ count: {
1358
+ type: "number",
1359
+ description: "Number of candles to return (max 200)"
1360
+ },
1361
+ convertingPriceUnit: {
1362
+ type: "string",
1363
+ description: "Price unit for conversion"
1364
+ }
1365
+ },
1366
+ required: ["market"]
1367
+ },
1368
+ handler: async (rawArgs, context) => {
1369
+ const args = asRecord(rawArgs);
1370
+ const response = await context.client.publicGet(
1371
+ "/v1/candles/days",
1372
+ compactObject({
1373
+ market: requireString(args, "market"),
1374
+ to: readString(args, "to"),
1375
+ count: readNumber(args, "count"),
1376
+ convertingPriceUnit: readString(args, "convertingPriceUnit")
1377
+ }),
1378
+ publicRateLimit("market_get_candles_days")
1379
+ );
1380
+ return normalizeResponse(response);
1381
+ }
1382
+ },
1383
+ // ── 7. market_get_candles_weeks ────────────────────────────────
1384
+ {
1385
+ name: "market_get_candles_weeks",
1386
+ module: "market",
1387
+ description: "\uC8FC(week) \uCE94\uB4E4\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. Get weekly candles.",
1388
+ isWrite: false,
1389
+ inputSchema: {
1390
+ type: "object",
1391
+ properties: {
1392
+ market: {
1393
+ type: "string",
1394
+ description: "Market code, e.g. KRW-BTC"
1395
+ },
1396
+ to: {
1397
+ type: "string",
1398
+ description: "Return candles before this timestamp"
1399
+ },
1400
+ count: {
1401
+ type: "number",
1402
+ description: "Number of candles to return (max 200)"
1403
+ }
1404
+ },
1405
+ required: ["market"]
1406
+ },
1407
+ handler: async (rawArgs, context) => {
1408
+ const args = asRecord(rawArgs);
1409
+ const response = await context.client.publicGet(
1410
+ "/v1/candles/weeks",
1411
+ compactObject({
1412
+ market: requireString(args, "market"),
1413
+ to: readString(args, "to"),
1414
+ count: readNumber(args, "count")
1415
+ }),
1416
+ publicRateLimit("market_get_candles_weeks")
1417
+ );
1418
+ return normalizeResponse(response);
1419
+ }
1420
+ },
1421
+ // ── 8. market_get_candles_months ───────────────────────────────
1422
+ {
1423
+ name: "market_get_candles_months",
1424
+ module: "market",
1425
+ description: "\uC6D4(month) \uCE94\uB4E4\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. Get monthly candles.",
1426
+ isWrite: false,
1427
+ inputSchema: {
1428
+ type: "object",
1429
+ properties: {
1430
+ market: {
1431
+ type: "string",
1432
+ description: "Market code, e.g. KRW-BTC"
1433
+ },
1434
+ to: {
1435
+ type: "string",
1436
+ description: "Return candles before this timestamp"
1437
+ },
1438
+ count: {
1439
+ type: "number",
1440
+ description: "Number of candles to return (max 200)"
1441
+ }
1442
+ },
1443
+ required: ["market"]
1444
+ },
1445
+ handler: async (rawArgs, context) => {
1446
+ const args = asRecord(rawArgs);
1447
+ const response = await context.client.publicGet(
1448
+ "/v1/candles/months",
1449
+ compactObject({
1450
+ market: requireString(args, "market"),
1451
+ to: readString(args, "to"),
1452
+ count: readNumber(args, "count")
1453
+ }),
1454
+ publicRateLimit("market_get_candles_months")
1455
+ );
1456
+ return normalizeResponse(response);
1457
+ }
1458
+ },
1459
+ // ── 9. market_get_warnings ─────────────────────────────────────
1460
+ {
1461
+ name: "market_get_warnings",
1462
+ module: "market",
1463
+ description: "\uD22C\uC790\uACBD\uBCF4 \uB9C8\uCF13 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. Get virtual asset warning market list.",
1464
+ isWrite: false,
1465
+ inputSchema: {
1466
+ type: "object",
1467
+ properties: {},
1468
+ required: []
1469
+ },
1470
+ handler: async (_rawArgs, context) => {
1471
+ const response = await context.client.publicGet(
1472
+ "/v1/market/virtual_asset_warning",
1473
+ {},
1474
+ publicRateLimit("market_get_warnings")
1475
+ );
1476
+ return normalizeResponse(response);
1477
+ }
1478
+ },
1479
+ // ── 10. market_get_notices ─────────────────────────────────────
1480
+ {
1481
+ name: "market_get_notices",
1482
+ module: "market",
1483
+ description: "\uACF5\uC9C0\uC0AC\uD56D \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. Get notice list.",
1484
+ isWrite: false,
1485
+ inputSchema: {
1486
+ type: "object",
1487
+ properties: {
1488
+ count: {
1489
+ type: "number",
1490
+ description: "Number of notices to return (min 1, max 20, default 5)"
1491
+ }
1492
+ },
1493
+ required: []
1494
+ },
1495
+ handler: async (rawArgs, context) => {
1496
+ const args = asRecord(rawArgs);
1497
+ const count = readNumber(args, "count");
1498
+ const response = await context.client.publicGet(
1499
+ "/v1/notices",
1500
+ compactObject({ count }),
1501
+ publicRateLimit("market_get_notices")
1502
+ );
1503
+ return normalizeResponse(response);
1504
+ }
1505
+ },
1506
+ // ── 11. market_get_fee_inout ───────────────────────────────────
1507
+ {
1508
+ name: "market_get_fee_inout",
1509
+ module: "market",
1510
+ description: "\uC785\uCD9C\uAE08 \uC218\uC218\uB8CC\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. Get deposit/withdrawal fee info for a currency.",
1511
+ isWrite: false,
1512
+ inputSchema: {
1513
+ type: "object",
1514
+ properties: {
1515
+ currency: {
1516
+ type: "string",
1517
+ description: 'Currency symbol (use "ALL" to retrieve all currencies), e.g. BTC, ETH'
1518
+ }
1519
+ },
1520
+ required: ["currency"]
1521
+ },
1522
+ handler: async (rawArgs, context) => {
1523
+ const args = asRecord(rawArgs);
1524
+ const currency = requireString(args, "currency");
1525
+ const response = await context.client.publicGet(
1526
+ `/v2/fee/inout/${currency}`,
1527
+ {},
1528
+ publicRateLimit("market_get_fee_inout")
1529
+ );
1530
+ return normalizeResponse(response);
1531
+ }
1532
+ }
1533
+ ];
1534
+ }
1535
+ function registerAccountTools() {
1536
+ return [
1537
+ // ── 1. account_get_balance ─────────────────────────────────────
1538
+ {
1539
+ name: "account_get_balance",
1540
+ module: "account",
1541
+ description: "\uC804\uCCB4 \uACC4\uC88C \uC794\uACE0\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. Get all account balances. Returns all currencies with balance, locked amounts, and avg buy price. Run before any trade or withdrawal to verify available funds.",
1542
+ isWrite: false,
1543
+ inputSchema: {
1544
+ type: "object",
1545
+ properties: {},
1546
+ required: []
1547
+ },
1548
+ handler: async (_rawArgs, context) => {
1549
+ const response = await context.client.privateGet(
1550
+ "/v1/accounts",
1551
+ void 0,
1552
+ privateRateLimit("account_get_balance")
1553
+ );
1554
+ return normalizeResponse(response);
1555
+ }
1556
+ },
1557
+ // ── 2. account_get_order_chance ────────────────────────────────
1558
+ {
1559
+ name: "account_get_order_chance",
1560
+ module: "account",
1561
+ description: "\uC8FC\uBB38 \uAC00\uB2A5 \uC815\uBCF4\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. Get order chance info (available balance, fees, limits) for a market. Run before placing any order to confirm available balance, bid/ask fee rates, and min/max order size constraints.",
1562
+ isWrite: false,
1563
+ inputSchema: {
1564
+ type: "object",
1565
+ properties: {
1566
+ market: {
1567
+ type: "string",
1568
+ description: "Market code, e.g. KRW-BTC"
1569
+ }
1570
+ },
1571
+ required: ["market"]
1572
+ },
1573
+ handler: async (rawArgs, context) => {
1574
+ const args = asRecord(rawArgs);
1575
+ const response = await context.client.privateGet(
1576
+ "/v1/orders/chance",
1577
+ compactObject({ market: requireString(args, "market") }),
1578
+ privateRateLimit("account_get_order_chance")
1579
+ );
1580
+ return normalizeResponse(response);
1581
+ }
1582
+ },
1583
+ // ── 3. account_get_wallet_status ───────────────────────────────
1584
+ {
1585
+ name: "account_get_wallet_status",
1586
+ module: "account",
1587
+ description: "\uC785\uCD9C\uAE08 \uD604\uD669\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. Get wallet status (block status, deposit/withdrawal availability). Run before any deposit or withdrawal to verify the blockchain is synced and operations are not disabled for maintenance.",
1588
+ isWrite: false,
1589
+ inputSchema: {
1590
+ type: "object",
1591
+ properties: {},
1592
+ required: []
1593
+ },
1594
+ handler: async (_rawArgs, context) => {
1595
+ const response = await context.client.privateGet(
1596
+ "/v1/status/wallet",
1597
+ {},
1598
+ privateRateLimit("account_get_wallet_status")
1599
+ );
1600
+ return normalizeResponse(response);
1601
+ }
1602
+ },
1603
+ // ── 4. account_get_api_keys ────────────────────────────────────
1604
+ {
1605
+ name: "account_get_api_keys",
1606
+ module: "account",
1607
+ description: "API \uD0A4 \uBAA9\uB85D\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. Get list of API keys and expiration dates. Check periodically to avoid unexpected authentication failures due to key expiration.",
1608
+ isWrite: false,
1609
+ inputSchema: {
1610
+ type: "object",
1611
+ properties: {},
1612
+ required: []
1613
+ },
1614
+ handler: async (_rawArgs, context) => {
1615
+ const response = await context.client.privateGet(
1616
+ "/v1/api_keys",
1617
+ {},
1618
+ privateRateLimit("account_get_api_keys")
1619
+ );
1620
+ return normalizeResponse(response);
1621
+ }
1622
+ }
1623
+ ];
1624
+ }
1625
+ function registerTradeTools() {
1626
+ return [
1627
+ // ── 1. trade_get_order ─────────────────────────────────────────
1628
+ {
1629
+ name: "trade_get_order",
1630
+ module: "trade",
1631
+ description: "\uAC1C\uBCC4 \uC8FC\uBB38\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. Get a specific order by uuid or client_order_id.",
1632
+ isWrite: false,
1633
+ inputSchema: {
1634
+ type: "object",
1635
+ properties: {
1636
+ uuid: {
1637
+ type: "string",
1638
+ description: "Order UUID"
1639
+ },
1640
+ client_order_id: {
1641
+ type: "string",
1642
+ description: "Client-assigned order ID"
1643
+ }
1644
+ },
1645
+ required: []
1646
+ },
1647
+ handler: async (rawArgs, context) => {
1648
+ const args = asRecord(rawArgs);
1649
+ const response = await context.client.privateGet(
1650
+ "/v1/order",
1651
+ compactObject({
1652
+ uuid: readString(args, "uuid"),
1653
+ client_order_id: readString(args, "client_order_id")
1654
+ }),
1655
+ privateRateLimit("trade_get_order")
1656
+ );
1657
+ return normalizeResponse(response);
1658
+ }
1659
+ },
1660
+ // ── 2. trade_get_orders ────────────────────────────────────────
1661
+ {
1662
+ name: "trade_get_orders",
1663
+ module: "trade",
1664
+ description: "\uC8FC\uBB38 \uB9AC\uC2A4\uD2B8\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. Get list of orders with filters.",
1665
+ isWrite: false,
1666
+ inputSchema: {
1667
+ type: "object",
1668
+ properties: {
1669
+ market: {
1670
+ type: "string",
1671
+ description: "Market code, e.g. KRW-BTC"
1672
+ },
1673
+ state: {
1674
+ type: "string",
1675
+ enum: ["wait", "watch", "done", "cancel"],
1676
+ description: "Order state filter"
1677
+ },
1678
+ states: {
1679
+ type: "array",
1680
+ items: { type: "string" },
1681
+ description: "Multiple order state filters"
1682
+ },
1683
+ uuids: {
1684
+ type: "array",
1685
+ items: { type: "string" },
1686
+ description: "Filter by order UUIDs"
1687
+ },
1688
+ client_order_ids: {
1689
+ type: "array",
1690
+ items: { type: "string" },
1691
+ description: "Filter by client order IDs"
1692
+ },
1693
+ page: {
1694
+ type: "number",
1695
+ description: "Page number"
1696
+ },
1697
+ limit: {
1698
+ type: "number",
1699
+ description: "Number of results per page"
1700
+ },
1701
+ order_by: {
1702
+ type: "string",
1703
+ description: "Sort order"
1704
+ }
1705
+ },
1706
+ required: []
1707
+ },
1708
+ handler: async (rawArgs, context) => {
1709
+ const args = asRecord(rawArgs);
1710
+ const response = await context.client.privateGet(
1711
+ "/v1/orders",
1712
+ compactObject({
1713
+ market: readString(args, "market"),
1714
+ state: readString(args, "state"),
1715
+ states: readStringArray(args, "states"),
1716
+ uuids: readStringArray(args, "uuids"),
1717
+ client_order_ids: readStringArray(args, "client_order_ids"),
1718
+ page: readNumber(args, "page"),
1719
+ limit: readNumber(args, "limit"),
1720
+ order_by: readString(args, "order_by")
1721
+ }),
1722
+ privateRateLimit("trade_get_orders")
1723
+ );
1724
+ return normalizeResponse(response);
1725
+ }
1726
+ },
1727
+ // ── 3. trade_place_order ───────────────────────────────────────
1728
+ {
1729
+ name: "trade_place_order",
1730
+ module: "trade",
1731
+ description: '\uC8FC\uBB38\uC744 \uC0DD\uC131\uD569\uB2C8\uB2E4. Place a new spot order on Bithumb. Use when: user asks to buy/sell a single market at a specific price or quantity. Do NOT use: for multiple orders at once (use trade_batch_place); for time-weighted execution (use twap_place). Field: order_type \u2208 {limit, price, market}. limit: price+volume required. price(\uC2DC\uC7A5\uAC00 \uB9E4\uC218): only price (total KRW). market(\uC2DC\uC7A5\uAC00 \uB9E4\uB3C4): only volume (coin qty). Note: Bithumb API response field is `ord_type` (not `order_type`). \u26A0\uFE0F REQUIRES EXPLICIT USER CONFIRMATION before executing: show the user market, side, order_type, price, volume, and total notional, then wait for explicit approval. Do NOT place the order without user confirmation. Example call: {"market":"KRW-BTC","side":"bid","order_type":"limit","price":"50000000","volume":"0.0001"}',
1732
+ isWrite: true,
1733
+ inputSchema: {
1734
+ type: "object",
1735
+ properties: {
1736
+ market: {
1737
+ type: "string",
1738
+ description: "Market code, e.g. KRW-BTC"
1739
+ },
1740
+ side: {
1741
+ type: "string",
1742
+ enum: ["bid", "ask"],
1743
+ description: "Order side: bid (buy) or ask (sell)"
1744
+ },
1745
+ order_type: {
1746
+ type: "string",
1747
+ enum: ["limit", "price", "market"],
1748
+ description: "Order type: limit, price (market buy), market (market sell). Note: Bithumb API response field is `ord_type` (not `order_type`)."
1749
+ },
1750
+ price: {
1751
+ type: "string",
1752
+ description: "Order price (required for limit and price orders)"
1753
+ },
1754
+ volume: {
1755
+ type: "string",
1756
+ description: "Order volume (required for limit and market orders)"
1757
+ },
1758
+ client_order_id: {
1759
+ type: "string",
1760
+ description: "Client-assigned order ID for idempotency"
1761
+ }
1762
+ },
1763
+ required: ["market", "side", "order_type"]
1764
+ },
1765
+ handler: async (rawArgs, context) => {
1766
+ const args = asRecord(rawArgs);
1767
+ const response = await context.client.privatePost(
1768
+ "/v2/orders",
1769
+ compactObject({
1770
+ market: requireString(args, "market"),
1771
+ side: requireString(args, "side"),
1772
+ order_type: requireString(args, "order_type"),
1773
+ price: readString(args, "price"),
1774
+ volume: readString(args, "volume"),
1775
+ client_order_id: readString(args, "client_order_id")
1776
+ }),
1777
+ orderRateLimit("trade_place_order")
1778
+ );
1779
+ return normalizeResponse(response);
1780
+ }
1781
+ },
1782
+ // ── 4. trade_cancel_order ──────────────────────────────────────
1783
+ {
1784
+ name: "trade_cancel_order",
1785
+ module: "trade",
1786
+ description: "\uC8FC\uBB38\uC744 \uCDE8\uC18C\uD569\uB2C8\uB2E4. Cancel an order by order_id or client_order_id. \u26A0\uFE0F REQUIRES EXPLICIT USER CONFIRMATION before executing: show the user the order details (order_id or client_order_id) and wait for explicit approval. Do NOT cancel without user confirmation.",
1787
+ isWrite: true,
1788
+ inputSchema: {
1789
+ type: "object",
1790
+ properties: {
1791
+ order_id: {
1792
+ type: "string",
1793
+ description: "Order ID to cancel"
1794
+ },
1795
+ client_order_id: {
1796
+ type: "string",
1797
+ description: "Client-assigned order ID to cancel"
1798
+ }
1799
+ },
1800
+ required: []
1801
+ },
1802
+ handler: async (rawArgs, context) => {
1803
+ const args = asRecord(rawArgs);
1804
+ const response = await context.client.privateDelete(
1805
+ "/v2/order",
1806
+ compactObject({
1807
+ order_id: readString(args, "order_id"),
1808
+ client_order_id: readString(args, "client_order_id")
1809
+ }),
1810
+ orderRateLimit("trade_cancel_order")
1811
+ );
1812
+ return normalizeResponse(response);
1813
+ }
1814
+ },
1815
+ // ── 5. trade_batch_place ───────────────────────────────────────
1816
+ {
1817
+ name: "trade_batch_place",
1818
+ module: "trade",
1819
+ description: '\uB2E4\uAC74 \uC8FC\uBB38\uC744 \uC694\uCCAD\uD569\uB2C8\uB2E4. Place multiple orders in a single batch (max 20). Use when: user asks for multiple orders at once (e.g., grid orders, simultaneous BTC+ETH limit buys). Do NOT use: for a single order (use trade_place_order); for time-sliced execution (use twap_place). Each order item field: `order_type` (canonical) \u2208 {limit, price, market}. Partial-failure semantics: each item may succeed or fail independently. Do NOT auto-retry failed items; surface them to the user. \u26A0\uFE0F REQUIRES EXPLICIT USER CONFIRMATION before executing: read all orders back to the user (market, side, order_type, price, volume for each) and wait for explicit approval. Do NOT place any order without user confirmation. Example call: {"batch_orders":[{"market":"KRW-BTC","side":"bid","order_type":"limit","price":"50000000","volume":"0.0001"},{"market":"KRW-ETH","side":"bid","order_type":"limit","price":"4500000","volume":"0.001"}]}',
1820
+ isWrite: true,
1821
+ inputSchema: {
1822
+ type: "object",
1823
+ properties: {
1824
+ batch_orders: {
1825
+ type: "array",
1826
+ items: {
1827
+ type: "object",
1828
+ properties: {
1829
+ market: { type: "string", description: "Market code, e.g. KRW-BTC" },
1830
+ side: { type: "string", enum: ["bid", "ask"], description: "Order side" },
1831
+ order_type: { type: "string", enum: ["limit", "price", "market"], description: "Order type" },
1832
+ price: { type: "string", description: "Order price" },
1833
+ volume: { type: "string", description: "Order volume" },
1834
+ client_order_id: { type: "string", description: "Client-assigned order ID" }
1835
+ },
1836
+ required: ["market", "side", "order_type"]
1837
+ },
1838
+ description: "Array of order objects (max 20)"
1839
+ }
1840
+ },
1841
+ required: ["batch_orders"]
1842
+ },
1843
+ handler: async (rawArgs, context) => {
1844
+ const args = asRecord(rawArgs);
1845
+ const batchOrders = args.batch_orders;
1846
+ if (!Array.isArray(batchOrders) || batchOrders.length === 0) {
1847
+ throw new ValidationError(
1848
+ 'Missing required parameter "batch_orders".',
1849
+ "Provide an array of order objects."
1850
+ );
1851
+ }
1852
+ const response = await context.client.privatePost(
1853
+ "/v2/orders/batch",
1854
+ { batch_orders: batchOrders },
1855
+ orderRateLimit("trade_batch_place")
1856
+ );
1857
+ return normalizeResponse(response);
1858
+ }
1859
+ },
1860
+ // ── 6. trade_batch_cancel ──────────────────────────────────────
1861
+ {
1862
+ name: "trade_batch_cancel",
1863
+ module: "trade",
1864
+ description: "\uB2E4\uAC74 \uC8FC\uBB38\uC744 \uCDE8\uC18C\uD569\uB2C8\uB2E4. Cancel multiple orders (max 30). Provide order_ids or client_order_ids. \u26A0\uFE0F REQUIRES EXPLICIT USER CONFIRMATION before executing: show the user all order IDs to be cancelled and wait for explicit approval. Do NOT cancel without user confirmation.",
1865
+ isWrite: true,
1866
+ inputSchema: {
1867
+ type: "object",
1868
+ properties: {
1869
+ order_ids: {
1870
+ type: "array",
1871
+ items: { type: "string" },
1872
+ description: "List of order IDs to cancel (max 30)"
1873
+ },
1874
+ client_order_ids: {
1875
+ type: "array",
1876
+ items: { type: "string" },
1877
+ description: "List of client-assigned order IDs to cancel (max 30)"
1878
+ }
1879
+ },
1880
+ required: []
1881
+ },
1882
+ handler: async (rawArgs, context) => {
1883
+ const args = asRecord(rawArgs);
1884
+ const response = await context.client.privatePost(
1885
+ "/v2/orders/cancel",
1886
+ compactObject({
1887
+ order_ids: readStringArray(args, "order_ids"),
1888
+ client_order_ids: readStringArray(args, "client_order_ids")
1889
+ }),
1890
+ orderRateLimit("trade_batch_cancel")
1891
+ );
1892
+ return normalizeResponse(response);
1893
+ }
1894
+ }
1895
+ ];
1896
+ }
1897
+ var DEFAULT_LOG_DIR = path.join(os.homedir(), ".bithumb", "logs");
1898
+ function getLogPaths(logDir, days = 7) {
1899
+ const paths = [];
1900
+ const now = /* @__PURE__ */ new Date();
1901
+ for (let i = 0; i < days; i++) {
1902
+ const d = new Date(now);
1903
+ d.setDate(d.getDate() - i);
1904
+ const yyyy = d.getFullYear();
1905
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
1906
+ const dd = String(d.getDate()).padStart(2, "0");
1907
+ paths.push(path.join(logDir, `trade-${yyyy}-${mm}-${dd}.log`));
1908
+ }
1909
+ return paths;
1910
+ }
1911
+ function readEntries(logDir) {
1912
+ const filePaths = getLogPaths(logDir);
1913
+ const entries = [];
1914
+ for (const filePath of filePaths) {
1915
+ if (!fs.existsSync(filePath)) continue;
1916
+ let content;
1917
+ try {
1918
+ content = fs.readFileSync(filePath, "utf8");
1919
+ } catch {
1920
+ continue;
1921
+ }
1922
+ for (const line of content.split("\n")) {
1923
+ const trimmed = line.trim();
1924
+ if (!trimmed) continue;
1925
+ try {
1926
+ const entry = JSON.parse(trimmed);
1927
+ entries.push(entry);
1928
+ } catch {
1929
+ }
1930
+ }
1931
+ }
1932
+ return entries;
1933
+ }
1934
+ function extractTool(entry) {
1935
+ if (entry.tool) return entry.tool;
1936
+ if (typeof entry.message === "string" && entry.message.startsWith("tool:")) {
1937
+ return entry.message.slice("tool:".length);
1938
+ }
1939
+ return void 0;
1940
+ }
1941
+ function registerAuditTools(logDir) {
1942
+ const resolvedLogDir = logDir ?? DEFAULT_LOG_DIR;
1943
+ return [
1944
+ // ── system_get_audit_log ───────────────────────────────────────
1945
+ {
1946
+ name: "system_get_audit_log",
1947
+ module: "system",
1948
+ description: "\uB85C\uCEEC \uB85C\uADF8\uC5D0\uC11C \uAC70\uB798 \uC774\uB825\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. Read local audit logs filtered by tool, level, and time.",
1949
+ isWrite: false,
1950
+ inputSchema: {
1951
+ type: "object",
1952
+ properties: {
1953
+ limit: {
1954
+ type: "number",
1955
+ description: "Maximum number of entries to return (default 20)."
1956
+ },
1957
+ tool: {
1958
+ type: "string",
1959
+ description: "Filter by tool name."
1960
+ },
1961
+ level: {
1962
+ type: "string",
1963
+ enum: ["INFO", "WARN", "ERROR", "DEBUG"],
1964
+ description: "Filter by log level (case-insensitive)."
1965
+ },
1966
+ since: {
1967
+ type: "string",
1968
+ description: "ISO 8601 timestamp; return entries at or after this time."
1969
+ }
1970
+ },
1971
+ required: []
1972
+ },
1973
+ handler: async (rawArgs, _context) => {
1974
+ const args = asRecord(rawArgs);
1975
+ const limit = readNumber(args, "limit") ?? 20;
1976
+ const toolFilter = readString(args, "tool");
1977
+ const levelFilter = readString(args, "level")?.toLowerCase();
1978
+ const sinceStr = readString(args, "since");
1979
+ const sinceMs = sinceStr ? new Date(sinceStr).getTime() : void 0;
1980
+ const entries = readEntries(resolvedLogDir);
1981
+ const filtered = entries.filter((entry) => {
1982
+ if (toolFilter) {
1983
+ const t = extractTool(entry);
1984
+ if (!t || !t.includes(toolFilter)) return false;
1985
+ }
1986
+ if (levelFilter && entry.level !== levelFilter) return false;
1987
+ if (sinceMs !== void 0) {
1988
+ const entryMs = new Date(entry.ts).getTime();
1989
+ if (Number.isNaN(entryMs) || entryMs < sinceMs) return false;
1990
+ }
1991
+ return true;
1992
+ });
1993
+ filtered.sort((a, b) => {
1994
+ const ta = new Date(a.ts).getTime();
1995
+ const tb = new Date(b.ts).getTime();
1996
+ return tb - ta;
1997
+ });
1998
+ return {
1999
+ endpoint: "local:audit-log",
2000
+ requestTime: (/* @__PURE__ */ new Date()).toISOString(),
2001
+ data: filtered.slice(0, limit)
2002
+ };
2003
+ }
2004
+ }
2005
+ ];
2006
+ }
2007
+ async function checkApiReachability(baseUrl) {
2008
+ try {
2009
+ const controller = new AbortController();
2010
+ const timeout = setTimeout(() => controller.abort(), 5e3);
2011
+ const res = await fetch(`${baseUrl}/v1/market/all`, {
2012
+ signal: controller.signal
2013
+ });
2014
+ clearTimeout(timeout);
2015
+ return {
2016
+ name: "API Reachability",
2017
+ status: res.ok ? "pass" : "fail",
2018
+ message: res.ok ? `${baseUrl} reachable (HTTP ${res.status})` : `${baseUrl} returned HTTP ${res.status}`
2019
+ };
2020
+ } catch (err) {
2021
+ return {
2022
+ name: "API Reachability",
2023
+ status: "fail",
2024
+ message: `Cannot reach ${baseUrl}: ${err instanceof Error ? err.message : String(err)}`
2025
+ };
2026
+ }
2027
+ }
2028
+ function checkAuthentication() {
2029
+ const accessKey = process.env.BITHUMB_ACCESS_KEY?.trim();
2030
+ const secretKey = process.env.BITHUMB_SECRET_KEY?.trim();
2031
+ if (accessKey && secretKey) {
2032
+ return { name: "Authentication", status: "pass", message: "API keys configured via environment variables" };
2033
+ }
2034
+ const tomlPath = join(homedir(), ".bithumb", "config.toml");
2035
+ if (existsSync(tomlPath)) {
2036
+ return { name: "Authentication", status: "pass", message: "config.toml found (credentials may be in profile)" };
2037
+ }
2038
+ if (accessKey || secretKey) {
2039
+ return { name: "Authentication", status: "fail", message: "Partial credentials: set both BITHUMB_ACCESS_KEY and BITHUMB_SECRET_KEY" };
2040
+ }
2041
+ return { name: "Authentication", status: "fail", message: "No credentials found (set env vars or create ~/.bithumb/config.toml)" };
2042
+ }
2043
+ function checkTomlConfig() {
2044
+ const tomlPath = join(homedir(), ".bithumb", "config.toml");
2045
+ if (existsSync(tomlPath)) {
2046
+ return { name: "TOML Config", status: "pass", message: `Found ${tomlPath}` };
2047
+ }
2048
+ return { name: "TOML Config", status: "fail", message: `Not found: ${tomlPath} (optional \u2014 use 'setup' command to create)` };
2049
+ }
2050
+ function checkModules(enabledModules) {
2051
+ return {
2052
+ name: "Enabled Modules",
2053
+ status: "pass",
2054
+ message: `Active: ${enabledModules.join(", ")}`
2055
+ };
2056
+ }
2057
+ function registerDiagnoseTools() {
2058
+ return [
2059
+ {
2060
+ name: "system_get_capabilities",
2061
+ module: "system",
2062
+ description: "Return server capabilities and module availability for agent planning.",
2063
+ isWrite: false,
2064
+ inputSchema: {
2065
+ type: "object",
2066
+ properties: {},
2067
+ additionalProperties: false
2068
+ },
2069
+ handler: async (_args, context) => {
2070
+ const enabledModules = new Set(context.config.modules);
2071
+ const moduleAvailability = {};
2072
+ for (const mod of MODULES) {
2073
+ if (!enabledModules.has(mod)) {
2074
+ moduleAvailability[mod] = { status: "disabled", reasonCode: "MODULE_FILTERED" };
2075
+ } else if (mod !== "market" && mod !== "system" && !context.config.hasAuth) {
2076
+ moduleAvailability[mod] = { status: "requires_auth", reasonCode: "AUTH_MISSING" };
2077
+ } else {
2078
+ moduleAvailability[mod] = { status: "enabled" };
2079
+ }
2080
+ }
2081
+ return {
2082
+ endpoint: "local:capabilities",
2083
+ requestTime: (/* @__PURE__ */ new Date()).toISOString(),
2084
+ data: {
2085
+ readOnly: context.config.readOnly,
2086
+ hasAuth: context.config.hasAuth,
2087
+ moduleAvailability
2088
+ }
2089
+ };
2090
+ }
2091
+ },
2092
+ {
2093
+ name: "system_diagnose",
2094
+ module: "system",
2095
+ description: "Run diagnostic checks on the Bithumb Trade Kit configuration. Checks API reachability, authentication, TOML config, and module status.",
2096
+ isWrite: false,
2097
+ inputSchema: {
2098
+ type: "object",
2099
+ properties: {}
2100
+ },
2101
+ handler: async (_args, context) => {
2102
+ const baseUrl = context?.config?.baseUrl ?? BITHUMB_API_BASE_URL;
2103
+ const modules = context?.config?.modules ?? [];
2104
+ const checks = [];
2105
+ checks.push(await checkApiReachability(baseUrl));
2106
+ checks.push(checkAuthentication());
2107
+ checks.push(checkTomlConfig());
2108
+ checks.push(checkModules(modules));
2109
+ const passed = checks.filter((c) => c.status === "pass").length;
2110
+ const total = checks.length;
2111
+ return {
2112
+ endpoint: "local:diagnose",
2113
+ requestTime: (/* @__PURE__ */ new Date()).toISOString(),
2114
+ data: {
2115
+ summary: `${passed}/${total} checks passed`,
2116
+ checks
2117
+ }
2118
+ };
2119
+ }
2120
+ }
2121
+ ];
2122
+ }
2123
+ function registerTwapTools() {
2124
+ return [
2125
+ // ── 6. twap_place_order ────────────────────────────────────────
2126
+ {
2127
+ name: "twap_place_order",
2128
+ module: "twap",
2129
+ description: "TWAP \uC8FC\uBB38\uC744 \uC694\uCCAD\uD569\uB2C8\uB2E4. Place a TWAP (Time-Weighted Average Price) order. \u26A0\uFE0F REQUIRES EXPLICIT USER CONFIRMATION before executing: show the user market, side, price/volume, duration, frequency, and computed slice count (duration \xF7 frequency), then wait for explicit approval. Do NOT place the order without user confirmation.",
2130
+ isWrite: true,
2131
+ inputSchema: {
2132
+ type: "object",
2133
+ properties: {
2134
+ market: {
2135
+ type: "string",
2136
+ description: "\uAC70\uB798 \uB300\uC0C1 \uD398\uC5B4\uC758 \uACE0\uC720 \uC2EC\uBCFC (\uC608\uC2DC: KRW-BTC)"
2137
+ },
2138
+ side: {
2139
+ type: "string",
2140
+ enum: ["bid", "ask"],
2141
+ description: "\uC8FC\uBB38 \uC885\uB958: bid (\uB9E4\uC218), ask (\uB9E4\uB3C4)"
2142
+ },
2143
+ duration: {
2144
+ type: "string",
2145
+ description: "\uC8FC\uBB38 \uC2DC\uAC04 - TWAP \uC8FC\uBB38\uC774 \uC9C4\uD589\uB418\uB294 \uC2DC\uAC04(\uCD08). min 300, max 43200"
2146
+ },
2147
+ frequency: {
2148
+ type: "string",
2149
+ enum: ["5", "15", "20", "30", "60", "120"],
2150
+ description: "\uC8FC\uBB38 \uAC04\uACA9(\uCD08)"
2151
+ },
2152
+ volume: {
2153
+ type: "string",
2154
+ description: "\uC8FC\uBB38 \uC218\uB7C9 (\uB9E4\uB3C4 \uC2DC \uD544\uC218)"
2155
+ },
2156
+ price: {
2157
+ type: "string",
2158
+ description: "\uC8FC\uBB38 \uAC00\uACA9 (\uB9E4\uC218 \uC2DC \uD544\uC218)"
2159
+ }
2160
+ },
2161
+ required: ["market", "side", "duration", "frequency"]
2162
+ },
2163
+ handler: async (rawArgs, context) => {
2164
+ const args = asRecord(rawArgs);
2165
+ const response = await context.client.privatePost(
2166
+ "/v1/twap",
2167
+ compactObject({
2168
+ market: requireString(args, "market"),
2169
+ side: requireString(args, "side"),
2170
+ duration: requireString(args, "duration"),
2171
+ frequency: requireString(args, "frequency"),
2172
+ volume: readString(args, "volume"),
2173
+ price: readString(args, "price")
2174
+ }),
2175
+ orderRateLimit("twap_place_order")
2176
+ );
2177
+ return normalizeResponse(response);
2178
+ }
2179
+ },
2180
+ // ── 7. twap_get_orders ─────────────────────────────────────────
2181
+ {
2182
+ name: "twap_get_orders",
2183
+ module: "twap",
2184
+ description: "TWAP \uC8FC\uBB38 \uB0B4\uC5ED\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. Get TWAP order history. Use after twap_place_order to verify the order is active (state: progress). Default state filter is 'progress'; pass state: 'done' or 'cancel' to query completed or cancelled orders.",
2185
+ isWrite: false,
2186
+ inputSchema: {
2187
+ type: "object",
2188
+ properties: {
2189
+ market: {
2190
+ type: "string",
2191
+ description: "\uAC70\uB798 \uB300\uC0C1 \uD398\uC5B4\uC758 \uACE0\uC720 \uC2EC\uBCFC (\uC608\uC2DC: KRW-BTC)"
2192
+ },
2193
+ uuids: {
2194
+ type: "array",
2195
+ items: { type: "string" },
2196
+ description: "TWAP \uC8FC\uBB38 ID \uBAA9\uB85D"
2197
+ },
2198
+ state: {
2199
+ type: "string",
2200
+ enum: ["progress", "done", "cancel"],
2201
+ description: "\uC8FC\uBB38 \uC0C1\uD0DC: progress (\uC9C4\uD589\uC911, default), done (\uC644\uB8CC), cancel (\uCDE8\uC18C)"
2202
+ },
2203
+ next_key: {
2204
+ type: "string",
2205
+ description: "\uB2E4\uC74C \uD398\uC774\uC9C0 \uC870\uD68C\uB97C \uC704\uD55C \uCEE4\uC11C \uAC12"
2206
+ },
2207
+ limit: {
2208
+ type: "number",
2209
+ description: "\uAC1C\uC218 \uC81C\uD55C (max 100)"
2210
+ },
2211
+ order_by: {
2212
+ type: "string",
2213
+ enum: ["asc", "desc"],
2214
+ description: "\uC870\uD68C \uACB0\uACFC \uC815\uB82C \uBC29\uC2DD: asc (\uC624\uB984\uCC28\uC21C), desc (\uB0B4\uB9BC\uCC28\uC21C, default)"
2215
+ }
2216
+ },
2217
+ required: []
2218
+ },
2219
+ handler: async (rawArgs, context) => {
2220
+ const args = asRecord(rawArgs);
2221
+ const response = await context.client.privateGet(
2222
+ "/v1/twap",
2223
+ compactObject({
2224
+ market: readString(args, "market"),
2225
+ uuids: readStringArray(args, "uuids"),
2226
+ state: readString(args, "state"),
2227
+ next_key: readString(args, "next_key"),
2228
+ limit: readNumber(args, "limit"),
2229
+ order_by: readString(args, "order_by")
2230
+ }),
2231
+ privateRateLimit("twap_get_orders")
2232
+ );
2233
+ return normalizeResponse(response);
2234
+ }
2235
+ },
2236
+ // ── 8. twap_cancel_order ───────────────────────────────────────
2237
+ {
2238
+ name: "twap_cancel_order",
2239
+ module: "twap",
2240
+ description: "TWAP \uC8FC\uBB38\uC744 \uCDE8\uC18C\uD569\uB2C8\uB2E4. Cancel a TWAP order. \u26A0\uFE0F REQUIRES EXPLICIT USER CONFIRMATION before executing: show the user the TWAP order ID and wait for explicit approval. Do NOT cancel without user confirmation.",
2241
+ isWrite: true,
2242
+ inputSchema: {
2243
+ type: "object",
2244
+ properties: {
2245
+ algo_order_id: {
2246
+ type: "string",
2247
+ description: "\uCDE8\uC18C\uD560 TWAP \uC8FC\uBB38 ID"
2248
+ }
2249
+ },
2250
+ required: ["algo_order_id"]
2251
+ },
2252
+ handler: async (rawArgs, context) => {
2253
+ const args = asRecord(rawArgs);
2254
+ const algo_order_id = requireString(args, "algo_order_id");
2255
+ const response = await context.client.privateDelete(
2256
+ "/v1/twap",
2257
+ { algo_order_id },
2258
+ orderRateLimit("twap_cancel_order")
2259
+ );
2260
+ return normalizeResponse(response);
2261
+ }
2262
+ }
2263
+ ];
2264
+ }
2265
+ function registerWithdrawTools() {
2266
+ return [
2267
+ // ── 9. withdraw_get_chance ───────────────────────────────────────
2268
+ {
2269
+ name: "withdraw_get_chance",
2270
+ module: "withdraw",
2271
+ description: "\uCD9C\uAE08 \uAC00\uB2A5 \uC815\uBCF4\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. Get withdrawal chance info (available balance, fees, limits). Always call this before withdraw_coin to confirm supported net_type values, withdrawal fee, minimum amount, and daily limits. For multi-network coins (USDT, USDC, XRP, etc.), this is the only way to discover valid net_type values \u2014 never guess.",
2272
+ isWrite: false,
2273
+ inputSchema: {
2274
+ type: "object",
2275
+ properties: {
2276
+ currency: {
2277
+ type: "string",
2278
+ description: "Currency symbol, e.g. BTC"
2279
+ },
2280
+ net_type: {
2281
+ type: "string",
2282
+ description: "Withdrawal network, e.g. BTC, DASH"
2283
+ }
2284
+ },
2285
+ required: ["currency", "net_type"]
2286
+ },
2287
+ handler: async (rawArgs, context) => {
2288
+ const args = asRecord(rawArgs);
2289
+ const response = await context.client.privateGet(
2290
+ "/v1/withdraws/chance",
2291
+ {
2292
+ currency: requireString(args, "currency"),
2293
+ net_type: requireString(args, "net_type")
2294
+ },
2295
+ privateRateLimit("withdraw_get_chance")
2296
+ );
2297
+ return normalizeResponse(response);
2298
+ }
2299
+ },
2300
+ // ── 10. withdraw_get ─────────────────────────────────────────────
2301
+ {
2302
+ name: "withdraw_get",
2303
+ module: "withdraw",
2304
+ description: "\uAC1C\uBCC4 \uCD9C\uAE08\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. Get a specific withdrawal by currency. Use when: you have a specific withdrawal UUID or txid. Do NOT use: to search withdrawal history or filter by state \u2014 use withdraw_get_list instead.",
2305
+ isWrite: false,
2306
+ inputSchema: {
2307
+ type: "object",
2308
+ properties: {
2309
+ currency: {
2310
+ type: "string",
2311
+ description: "Currency symbol, e.g. BTC"
2312
+ },
2313
+ uuid: {
2314
+ type: "string",
2315
+ description: "Withdrawal unique ID"
2316
+ },
2317
+ txid: {
2318
+ type: "string",
2319
+ description: "Withdrawal transaction ID"
2320
+ }
2321
+ },
2322
+ required: ["currency"]
2323
+ },
2324
+ handler: async (rawArgs, context) => {
2325
+ const args = asRecord(rawArgs);
2326
+ const response = await context.client.privateGet(
2327
+ "/v1/withdraw",
2328
+ compactObject({
2329
+ currency: requireString(args, "currency"),
2330
+ uuid: readString(args, "uuid"),
2331
+ txid: readString(args, "txid")
2332
+ }),
2333
+ privateRateLimit("withdraw_get")
2334
+ );
2335
+ return normalizeResponse(response);
2336
+ }
2337
+ },
2338
+ // ── 11. withdraw_get_list ────────────────────────────────────────
2339
+ {
2340
+ name: "withdraw_get_list",
2341
+ module: "withdraw",
2342
+ description: "\uCD9C\uAE08 \uB9AC\uC2A4\uD2B8\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. Get list of coin withdrawals.",
2343
+ isWrite: false,
2344
+ inputSchema: {
2345
+ type: "object",
2346
+ properties: {
2347
+ currency: {
2348
+ type: "string",
2349
+ description: "Currency symbol, e.g. BTC"
2350
+ },
2351
+ state: {
2352
+ type: "string",
2353
+ enum: ["PROCESSING", "DONE", "CANCELED"],
2354
+ description: "Withdrawal state filter"
2355
+ },
2356
+ uuids: {
2357
+ type: "array",
2358
+ items: { type: "string" },
2359
+ description: "Filter by withdrawal UUIDs"
2360
+ },
2361
+ txids: {
2362
+ type: "array",
2363
+ items: { type: "string" },
2364
+ description: "Filter by transaction IDs"
2365
+ },
2366
+ limit: {
2367
+ type: "number",
2368
+ description: "Number of results per page (max 100)"
2369
+ },
2370
+ page: {
2371
+ type: "number",
2372
+ description: "Page number"
2373
+ },
2374
+ order_by: {
2375
+ type: "string",
2376
+ enum: ["asc", "desc"],
2377
+ description: "Sort order (default: desc)"
2378
+ }
2379
+ },
2380
+ required: []
2381
+ },
2382
+ handler: async (rawArgs, context) => {
2383
+ const args = asRecord(rawArgs);
2384
+ const response = await context.client.privateGet(
2385
+ "/v1/withdraws",
2386
+ compactObject({
2387
+ currency: readString(args, "currency"),
2388
+ state: readString(args, "state"),
2389
+ uuids: readStringArray(args, "uuids"),
2390
+ txids: readStringArray(args, "txids"),
2391
+ limit: readNumber(args, "limit"),
2392
+ page: readNumber(args, "page"),
2393
+ order_by: readString(args, "order_by")
2394
+ }),
2395
+ privateRateLimit("withdraw_get_list")
2396
+ );
2397
+ return normalizeResponse(response);
2398
+ }
2399
+ },
2400
+ // ── 12. withdraw_get_list_krw ────────────────────────────────────
2401
+ {
2402
+ name: "withdraw_get_list_krw",
2403
+ module: "withdraw",
2404
+ description: "\uC6D0\uD654 \uCD9C\uAE08 \uB9AC\uC2A4\uD2B8\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. Get list of KRW withdrawals.",
2405
+ isWrite: false,
2406
+ inputSchema: {
2407
+ type: "object",
2408
+ properties: {
2409
+ state: {
2410
+ type: "string",
2411
+ enum: ["PROCESSING", "DONE", "CANCELED"],
2412
+ description: "Withdrawal state filter"
2413
+ },
2414
+ uuids: {
2415
+ type: "array",
2416
+ items: { type: "string" },
2417
+ description: "Filter by withdrawal UUIDs"
2418
+ },
2419
+ txids: {
2420
+ type: "array",
2421
+ items: { type: "string" },
2422
+ description: "Filter by transaction IDs"
2423
+ },
2424
+ limit: {
2425
+ type: "number",
2426
+ description: "Number of results per page (max 100)"
2427
+ },
2428
+ page: {
2429
+ type: "number",
2430
+ description: "Page number"
2431
+ },
2432
+ order_by: {
2433
+ type: "string",
2434
+ enum: ["asc", "desc"],
2435
+ description: "Sort order (default: desc)"
2436
+ }
2437
+ },
2438
+ required: []
2439
+ },
2440
+ handler: async (rawArgs, context) => {
2441
+ const args = asRecord(rawArgs);
2442
+ const response = await context.client.privateGet(
2443
+ "/v1/withdraws/krw",
2444
+ compactObject({
2445
+ state: readString(args, "state"),
2446
+ uuids: readStringArray(args, "uuids"),
2447
+ txids: readStringArray(args, "txids"),
2448
+ limit: readNumber(args, "limit"),
2449
+ page: readNumber(args, "page"),
2450
+ order_by: readString(args, "order_by")
2451
+ }),
2452
+ privateRateLimit("withdraw_get_list_krw")
2453
+ );
2454
+ return normalizeResponse(response);
2455
+ }
2456
+ },
2457
+ // ── 13. withdraw_coin ────────────────────────────────────────────
2458
+ {
2459
+ name: "withdraw_coin",
2460
+ module: "withdraw",
2461
+ description: "\uC2E4\uC81C \uAC00\uC0C1 \uC790\uC0B0\uC744 \uCD9C\uAE08\uD569\uB2C8\uB2E4. \uC774 \uC791\uC5C5\uC740 \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. Withdraw cryptocurrency. This action is IRREVERSIBLE. Use when: user has explicitly approved a withdrawal AND the full pre-flight checklist has run (account_get_balance, account_get_wallet_status, withdraw_get_chance, withdraw_get_addresses, market_get_fee_inout). Do NOT use: without prior `withdraw_get_chance` to confirm net_type/min/fee; without `withdraw_get_addresses` to confirm destination is in the allow-list; without explicit user confirmation of the full destination string. Multi-network coins (USDT/USDC/XRP): always run `withdraw_get_chance` first to discover supported `net_type`. Wrong net_type = permanent loss. secondary_address (memo/tag): mandatory for XRP/EOS/ATOM-class coins. Missing it = lost funds.",
2462
+ isWrite: true,
2463
+ inputSchema: {
2464
+ type: "object",
2465
+ properties: {
2466
+ currency: {
2467
+ type: "string",
2468
+ description: "Currency symbol, e.g. BTC"
2469
+ },
2470
+ net_type: {
2471
+ type: "string",
2472
+ description: "Withdrawal network, e.g. BTC, DASH"
2473
+ },
2474
+ amount: {
2475
+ type: "string",
2476
+ description: "Withdrawal amount"
2477
+ },
2478
+ address: {
2479
+ type: "string",
2480
+ description: "Registered withdrawal address"
2481
+ },
2482
+ secondary_address: {
2483
+ type: "string",
2484
+ description: "Secondary address (for certain assets)"
2485
+ },
2486
+ exchange_name: {
2487
+ type: "string",
2488
+ description: "Exchange name (English)"
2489
+ },
2490
+ receiver_type: {
2491
+ type: "string",
2492
+ enum: ["personal", "corporation"],
2493
+ description: "Receiver type: personal or corporation"
2494
+ },
2495
+ receiver_ko_name: {
2496
+ type: "string",
2497
+ description: "Receiver Korean name"
2498
+ },
2499
+ receiver_en_name: {
2500
+ type: "string",
2501
+ description: "Receiver English name"
2502
+ },
2503
+ receiver_corp_ko_name: {
2504
+ type: "string",
2505
+ description: "Corporation Korean name (required if corporation)"
2506
+ },
2507
+ receiver_corp_en_name: {
2508
+ type: "string",
2509
+ description: "Corporation English name (required if corporation)"
2510
+ }
2511
+ },
2512
+ required: ["currency", "net_type", "amount", "address"]
2513
+ },
2514
+ handler: async (rawArgs, context) => {
2515
+ const args = asRecord(rawArgs);
2516
+ const response = await context.client.privatePost(
2517
+ "/v1/withdraws/coin",
2518
+ compactObject({
2519
+ currency: requireString(args, "currency"),
2520
+ net_type: requireString(args, "net_type"),
2521
+ amount: requireString(args, "amount"),
2522
+ address: requireString(args, "address"),
2523
+ secondary_address: readString(args, "secondary_address"),
2524
+ exchange_name: readString(args, "exchange_name"),
2525
+ receiver_type: readString(args, "receiver_type"),
2526
+ receiver_ko_name: readString(args, "receiver_ko_name"),
2527
+ receiver_en_name: readString(args, "receiver_en_name"),
2528
+ receiver_corp_ko_name: readString(args, "receiver_corp_ko_name"),
2529
+ receiver_corp_en_name: readString(args, "receiver_corp_en_name")
2530
+ }),
2531
+ orderRateLimit("withdraw_coin")
2532
+ );
2533
+ return normalizeResponse(response);
2534
+ }
2535
+ },
2536
+ // ── 14. withdraw_krw ─────────────────────────────────────────────
2537
+ {
2538
+ name: "withdraw_krw",
2539
+ module: "withdraw",
2540
+ description: "\uB4F1\uB85D\uB41C \uACC4\uC88C\uB85C \uC6D0\uD654\uB97C \uCD9C\uAE08\uD569\uB2C8\uB2E4. \uC774 \uC791\uC5C5\uC740 \uB418\uB3CC\uB9B4 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. 2\uCC28 \uC778\uC99D(\uCE74\uCE74\uC624)\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. Withdraw KRW to registered bank account. This action is IRREVERSIBLE. Requires 2FA (Kakao). \u26A0\uFE0F REQUIRES EXPLICIT USER CONFIRMATION before executing: show the user the withdrawal amount and wait for explicit approval. Do NOT withdraw without user confirmation.",
2541
+ isWrite: true,
2542
+ inputSchema: {
2543
+ type: "object",
2544
+ properties: {
2545
+ amount: {
2546
+ type: "string",
2547
+ description: "Withdrawal amount in KRW"
2548
+ },
2549
+ two_factor_type: {
2550
+ type: "string",
2551
+ description: "2FA method (e.g. kakao)"
2552
+ }
2553
+ },
2554
+ required: ["amount", "two_factor_type"]
2555
+ },
2556
+ handler: async (rawArgs, context) => {
2557
+ const args = asRecord(rawArgs);
2558
+ const response = await context.client.privatePost(
2559
+ "/v1/withdraws/krw",
2560
+ {
2561
+ amount: requireString(args, "amount"),
2562
+ two_factor_type: requireString(args, "two_factor_type")
2563
+ },
2564
+ orderRateLimit("withdraw_krw")
2565
+ );
2566
+ return normalizeResponse(response);
2567
+ }
2568
+ },
2569
+ // ── 15. withdraw_cancel_coin ─────────────────────────────────────
2570
+ {
2571
+ name: "withdraw_cancel_coin",
2572
+ module: "withdraw",
2573
+ description: "\uAC00\uC0C1 \uC790\uC0B0 \uCD9C\uAE08\uC744 \uCDE8\uC18C\uD569\uB2C8\uB2E4. Cancel a cryptocurrency withdrawal. \u26A0\uFE0F REQUIRES EXPLICIT USER CONFIRMATION before executing: show the user the withdrawal ID and wait for explicit approval. Do NOT cancel without user confirmation.",
2574
+ isWrite: true,
2575
+ inputSchema: {
2576
+ type: "object",
2577
+ properties: {
2578
+ withdrawal_id: {
2579
+ type: "string",
2580
+ description: "Withdrawal unique ID to cancel"
2581
+ }
2582
+ },
2583
+ required: ["withdrawal_id"]
2584
+ },
2585
+ handler: async (rawArgs, context) => {
2586
+ const args = asRecord(rawArgs);
2587
+ const response = await context.client.privateDelete(
2588
+ "/v1/withdraws/coin",
2589
+ {
2590
+ withdrawal_id: requireString(args, "withdrawal_id")
2591
+ },
2592
+ orderRateLimit("withdraw_cancel_coin")
2593
+ );
2594
+ return normalizeResponse(response);
2595
+ }
2596
+ },
2597
+ // ── 16. withdraw_get_addresses ───────────────────────────────────
2598
+ {
2599
+ name: "withdraw_get_addresses",
2600
+ module: "withdraw",
2601
+ description: "\uCD9C\uAE08 \uD5C8\uC6A9 \uC8FC\uC18C \uB9AC\uC2A4\uD2B8\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. Get list of allowed withdrawal addresses.",
2602
+ isWrite: false,
2603
+ inputSchema: {
2604
+ type: "object",
2605
+ properties: {},
2606
+ required: []
2607
+ },
2608
+ handler: async (_rawArgs, context) => {
2609
+ const response = await context.client.privateGet(
2610
+ "/v1/withdraws/coin_addresses",
2611
+ {},
2612
+ privateRateLimit("withdraw_get_addresses")
2613
+ );
2614
+ return normalizeResponse(response);
2615
+ }
2616
+ }
2617
+ ];
2618
+ }
2619
+ function registerDepositTools() {
2620
+ return [
2621
+ // ── 17. deposit_get ───────────────────────────────────────────
2622
+ {
2623
+ name: "deposit_get",
2624
+ module: "deposit",
2625
+ description: '\uAC1C\uBCC4 \uC785\uAE08\uC744 \uC870\uD68C\uD569\uB2C8\uB2E4. Get a single deposit by currency + uuid (or single txid). Use when: you have a specific deposit UUID and want the full record. Do NOT use: to search by txid list \u2014 use deposit_get_list with txids (array) instead. Example call: {"currency":"BTC","uuid":"12345678-...."}',
2626
+ isWrite: false,
2627
+ inputSchema: {
2628
+ type: "object",
2629
+ properties: {
2630
+ currency: {
2631
+ type: "string",
2632
+ description: "Currency symbol, e.g. BTC"
2633
+ },
2634
+ uuid: {
2635
+ type: "string",
2636
+ description: "Deposit UUID"
2637
+ },
2638
+ txid: {
2639
+ type: "string",
2640
+ description: "Deposit TXID"
2641
+ }
2642
+ },
2643
+ required: ["currency"]
2644
+ },
2645
+ handler: async (rawArgs, context) => {
2646
+ const args = asRecord(rawArgs);
2647
+ const response = await context.client.privateGet(
2648
+ "/v1/deposit",
2649
+ compactObject({
2650
+ currency: requireString(args, "currency"),
2651
+ uuid: readString(args, "uuid"),
2652
+ txid: readString(args, "txid")
2653
+ }),
2654
+ privateRateLimit("deposit_get")
2655
+ );
2656
+ return normalizeResponse(response);
2657
+ }
2658
+ },
2659
+ // ── 18. deposit_get_list ──────────────────────────────────────
2660
+ {
2661
+ name: "deposit_get_list",
2662
+ module: "deposit",
2663
+ description: `\uC785\uAE08 \uB9AC\uC2A4\uD2B8\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. Get list of coin deposits with optional filters. Use when: user asks 'find this txid' / 'recent deposits' / 'list deposits in PROCESSING state'. Do NOT use: when you already have a deposit UUID and want a single record (use \`deposit_get\`). Example call (txid lookup): {"txids":["0xabc123..."]}`,
2664
+ isWrite: false,
2665
+ inputSchema: {
2666
+ type: "object",
2667
+ properties: {
2668
+ currency: {
2669
+ type: "string",
2670
+ description: "Currency symbol, e.g. BTC"
2671
+ },
2672
+ state: {
2673
+ type: "string",
2674
+ description: "Deposit state filter"
2675
+ },
2676
+ uuids: {
2677
+ type: "array",
2678
+ items: { type: "string" },
2679
+ description: "Filter by deposit UUIDs"
2680
+ },
2681
+ txids: {
2682
+ type: "array",
2683
+ items: { type: "string" },
2684
+ description: "Filter by deposit TXIDs"
2685
+ },
2686
+ limit: {
2687
+ type: "number",
2688
+ description: "Number of results (max 100)"
2689
+ },
2690
+ page: {
2691
+ type: "number",
2692
+ description: "Page number (default 1)"
2693
+ },
2694
+ order_by: {
2695
+ type: "string",
2696
+ description: "Sort order: asc or desc (default desc)"
2697
+ }
2698
+ },
2699
+ required: []
2700
+ },
2701
+ handler: async (rawArgs, context) => {
2702
+ const args = asRecord(rawArgs);
2703
+ const response = await context.client.privateGet(
2704
+ "/v1/deposits",
2705
+ compactObject({
2706
+ currency: readString(args, "currency"),
2707
+ state: readString(args, "state"),
2708
+ uuids: readStringArray(args, "uuids"),
2709
+ txids: readStringArray(args, "txids"),
2710
+ limit: readNumber(args, "limit"),
2711
+ page: readNumber(args, "page"),
2712
+ order_by: readString(args, "order_by")
2713
+ }),
2714
+ privateRateLimit("deposit_get_list")
2715
+ );
2716
+ return normalizeResponse(response);
2717
+ }
2718
+ },
2719
+ // ── 19. deposit_get_list_krw ──────────────────────────────────
2720
+ {
2721
+ name: "deposit_get_list_krw",
2722
+ module: "deposit",
2723
+ description: "\uC6D0\uD654 \uC785\uAE08 \uB9AC\uC2A4\uD2B8\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. Get list of KRW deposits.",
2724
+ isWrite: false,
2725
+ inputSchema: {
2726
+ type: "object",
2727
+ properties: {
2728
+ state: {
2729
+ type: "string",
2730
+ description: "Deposit state: PROCESSING, ACCEPTED, CANCELED"
2731
+ },
2732
+ uuids: {
2733
+ type: "array",
2734
+ items: { type: "string" },
2735
+ description: "Filter by deposit UUIDs"
2736
+ },
2737
+ txids: {
2738
+ type: "array",
2739
+ items: { type: "string" },
2740
+ description: "Filter by deposit TXIDs"
2741
+ },
2742
+ limit: {
2743
+ type: "number",
2744
+ description: "Number of results (max 100)"
2745
+ },
2746
+ page: {
2747
+ type: "number",
2748
+ description: "Page number (default 1)"
2749
+ },
2750
+ order_by: {
2751
+ type: "string",
2752
+ description: "Sort order: asc or desc (default desc)"
2753
+ }
2754
+ },
2755
+ required: []
2756
+ },
2757
+ handler: async (rawArgs, context) => {
2758
+ const args = asRecord(rawArgs);
2759
+ const response = await context.client.privateGet(
2760
+ "/v1/deposits/krw",
2761
+ compactObject({
2762
+ state: readString(args, "state"),
2763
+ uuids: readStringArray(args, "uuids"),
2764
+ txids: readStringArray(args, "txids"),
2765
+ limit: readNumber(args, "limit"),
2766
+ page: readNumber(args, "page"),
2767
+ order_by: readString(args, "order_by")
2768
+ }),
2769
+ privateRateLimit("deposit_get_list_krw")
2770
+ );
2771
+ return normalizeResponse(response);
2772
+ }
2773
+ },
2774
+ // ── 20. deposit_krw ──────────────────────────────────────────
2775
+ {
2776
+ name: "deposit_krw",
2777
+ module: "deposit",
2778
+ description: "\uC6D0\uD654 \uC785\uAE08\uC744 \uC694\uCCAD\uD569\uB2C8\uB2E4. 2\uCC28 \uC778\uC99D(\uCE74\uCE74\uC624)\uC774 \uD544\uC694\uD569\uB2C8\uB2E4. Request KRW deposit. Requires 2FA (Kakao). \u26A0\uFE0F REQUIRES EXPLICIT USER CONFIRMATION before executing: show the user the deposit amount and wait for explicit approval. Do NOT request deposit without user confirmation.",
2779
+ isWrite: true,
2780
+ inputSchema: {
2781
+ type: "object",
2782
+ properties: {
2783
+ amount: {
2784
+ type: "string",
2785
+ description: "Deposit amount in KRW"
2786
+ },
2787
+ two_factor_type: {
2788
+ type: "string",
2789
+ description: "2FA method, e.g. kakao"
2790
+ }
2791
+ },
2792
+ required: ["amount", "two_factor_type"]
2793
+ },
2794
+ handler: async (rawArgs, context) => {
2795
+ const args = asRecord(rawArgs);
2796
+ const response = await context.client.privatePost(
2797
+ "/v1/deposits/krw",
2798
+ {
2799
+ amount: requireString(args, "amount"),
2800
+ two_factor_type: requireString(args, "two_factor_type")
2801
+ },
2802
+ privateRateLimit("deposit_krw")
2803
+ );
2804
+ return normalizeResponse(response);
2805
+ }
2806
+ },
2807
+ // ── 21. deposit_generate_address ─────────────────────────────
2808
+ {
2809
+ name: "deposit_generate_address",
2810
+ module: "deposit",
2811
+ description: "\uC785\uAE08 \uC8FC\uC18C\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4. Generate a new deposit address. \u26A0\uFE0F REQUIRES EXPLICIT USER CONFIRMATION before executing: show the user the currency and network type, then wait for explicit approval. Do NOT generate without user confirmation.",
2812
+ isWrite: true,
2813
+ inputSchema: {
2814
+ type: "object",
2815
+ properties: {
2816
+ currency: {
2817
+ type: "string",
2818
+ description: "Currency symbol, e.g. BTC"
2819
+ },
2820
+ net_type: {
2821
+ type: "string",
2822
+ description: "Network type, e.g. BTC, ETH"
2823
+ }
2824
+ },
2825
+ required: ["currency", "net_type"]
2826
+ },
2827
+ handler: async (rawArgs, context) => {
2828
+ const args = asRecord(rawArgs);
2829
+ const response = await context.client.privatePost(
2830
+ "/v1/deposits/generate_coin_address",
2831
+ {
2832
+ currency: requireString(args, "currency"),
2833
+ net_type: requireString(args, "net_type")
2834
+ },
2835
+ privateRateLimit("deposit_generate_address")
2836
+ );
2837
+ return normalizeResponse(response);
2838
+ }
2839
+ },
2840
+ // ── 22. deposit_get_addresses ────────────────────────────────
2841
+ {
2842
+ name: "deposit_get_addresses",
2843
+ module: "deposit",
2844
+ description: "\uC804\uCCB4 \uC785\uAE08 \uC8FC\uC18C\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. Get all deposit addresses. Use this first for multi-network coins (USDT, USDC, XRP, etc.) to discover available currency + net_type combinations before calling deposit_get_address.",
2845
+ isWrite: false,
2846
+ inputSchema: {
2847
+ type: "object",
2848
+ properties: {},
2849
+ required: []
2850
+ },
2851
+ handler: async (_rawArgs, context) => {
2852
+ const response = await context.client.privateGet(
2853
+ "/v1/deposits/coin_addresses",
2854
+ {},
2855
+ privateRateLimit("deposit_get_addresses")
2856
+ );
2857
+ return normalizeResponse(response);
2858
+ }
2859
+ },
2860
+ // ── 23. deposit_get_address ──────────────────────────────────
2861
+ {
2862
+ name: "deposit_get_address",
2863
+ module: "deposit",
2864
+ description: "\uAC1C\uBCC4 \uC785\uAE08 \uC8FC\uC18C\uB97C \uC870\uD68C\uD569\uB2C8\uB2E4. Get deposit address for a specific currency and network. For multi-network coins (USDT, USDC, XRP, etc.), run deposit_get_addresses first to confirm the correct net_type \u2014 wrong network = permanent loss of funds.",
2865
+ isWrite: false,
2866
+ inputSchema: {
2867
+ type: "object",
2868
+ properties: {
2869
+ currency: {
2870
+ type: "string",
2871
+ description: "Currency symbol, e.g. BTC"
2872
+ },
2873
+ net_type: {
2874
+ type: "string",
2875
+ description: "Network type, e.g. BTC, ETH"
2876
+ }
2877
+ },
2878
+ required: ["currency", "net_type"]
2879
+ },
2880
+ handler: async (rawArgs, context) => {
2881
+ const args = asRecord(rawArgs);
2882
+ const response = await context.client.privateGet(
2883
+ "/v1/deposits/coin_address",
2884
+ {
2885
+ currency: requireString(args, "currency"),
2886
+ net_type: requireString(args, "net_type")
2887
+ },
2888
+ privateRateLimit("deposit_get_address")
2889
+ );
2890
+ return normalizeResponse(response);
2891
+ }
2892
+ }
2893
+ ];
2894
+ }
2895
+ function allToolSpecs() {
2896
+ return [
2897
+ ...registerMarketTools(),
2898
+ ...registerAccountTools(),
2899
+ ...registerTradeTools(),
2900
+ ...registerAuditTools(),
2901
+ ...registerDiagnoseTools(),
2902
+ ...registerTwapTools(),
2903
+ ...registerWithdrawTools(),
2904
+ ...registerDepositTools()
2905
+ ];
2906
+ }
2907
+ function buildTools(config) {
2908
+ const enabledModules = new Set(config.modules);
2909
+ const tools = allToolSpecs().filter((t) => enabledModules.has(t.module));
2910
+ return config.readOnly ? tools.filter((t) => !t.isWrite) : tools;
2911
+ }
2912
+ function toMcpTool(tool) {
2913
+ return {
2914
+ name: tool.name,
2915
+ description: tool.description,
2916
+ inputSchema: tool.inputSchema,
2917
+ annotations: {
2918
+ readOnlyHint: !tool.isWrite,
2919
+ destructiveHint: tool.isWrite,
2920
+ idempotentHint: !tool.isWrite,
2921
+ openWorldHint: true
2922
+ }
2923
+ };
2924
+ }
2925
+ function configFilePath() {
2926
+ return join2(homedir2(), ".bithumb", "config.toml");
2927
+ }
2928
+ function readFullConfig() {
2929
+ const path3 = configFilePath();
2930
+ if (!existsSync2(path3)) return { profiles: {} };
2931
+ const raw = readFileSync(path3, "utf-8");
2932
+ try {
2933
+ return parse(raw);
2934
+ } catch (err) {
2935
+ throw new ConfigError(
2936
+ `Failed to parse ${path3}: ${err instanceof Error ? err.message : String(err)}`,
2937
+ "Check TOML syntax in your config file, or delete and re-create it."
2938
+ );
2939
+ }
2940
+ }
2941
+ function readTomlProfile(profileName) {
2942
+ const config = readFullConfig();
2943
+ const name = profileName ?? config.default_profile ?? "default";
2944
+ return config.profiles?.[name] ?? {};
2945
+ }
2946
+ function loadConfig(options) {
2947
+ const toml = readTomlProfile(options?.profile);
2948
+ const accessKey = process.env.BITHUMB_ACCESS_KEY?.trim() ?? toml.access_key;
2949
+ const secretKey = process.env.BITHUMB_SECRET_KEY?.trim() ?? toml.secret_key;
2950
+ const hasAuth = Boolean(accessKey && secretKey);
2951
+ const partialAuth = Boolean(accessKey) || Boolean(secretKey);
2952
+ if (partialAuth && !hasAuth) {
2953
+ throw new ConfigError(
2954
+ "Partial API credentials.",
2955
+ "Set both BITHUMB_ACCESS_KEY and BITHUMB_SECRET_KEY (env vars or config.toml profile)."
2956
+ );
2957
+ }
2958
+ const baseUrl = (process.env.BITHUMB_API_BASE_URL?.trim() ?? toml.base_url ?? BITHUMB_API_BASE_URL).replace(/\/+$/, "");
2959
+ const rawTimeout = process.env.BITHUMB_TIMEOUT_MS ? Number(process.env.BITHUMB_TIMEOUT_MS) : toml.timeout_ms ?? 15e3;
2960
+ if (!Number.isFinite(rawTimeout) || rawTimeout <= 0) {
2961
+ throw new ConfigError(
2962
+ "Invalid timeout.",
2963
+ "BITHUMB_TIMEOUT_MS must be a positive integer."
2964
+ );
2965
+ }
2966
+ let modules = [...DEFAULT_MODULES];
2967
+ if (options?.modules) {
2968
+ const requested = options.modules.split(",").map((s) => s.trim()).filter(Boolean);
2969
+ if (requested.length > 0) {
2970
+ let isAll = false;
2971
+ for (const m of requested) {
2972
+ if (m === "all") {
2973
+ modules = [...MODULES];
2974
+ isAll = true;
2975
+ break;
2976
+ }
2977
+ if (!MODULES.includes(m)) {
2978
+ throw new ConfigError(
2979
+ `Unknown module "${m}".`,
2980
+ `Use: ${MODULES.join(", ")} or "all".`
2981
+ );
2982
+ }
2983
+ }
2984
+ if (!isAll) modules = requested;
2985
+ }
2986
+ }
2987
+ return {
2988
+ accessKey,
2989
+ secretKey,
2990
+ hasAuth,
2991
+ baseUrl,
2992
+ timeoutMs: Math.floor(rawTimeout),
2993
+ modules,
2994
+ readOnly: options?.readOnly ?? false,
2995
+ verbose: options?.verbose ?? false,
2996
+ clientType: options?.clientType ?? "etc"
2997
+ };
2998
+ }
2999
+ var LEVEL_ORDER = {
3000
+ debug: 0,
3001
+ info: 1,
3002
+ warn: 2,
3003
+ error: 3
3004
+ };
3005
+ var SENSITIVE_KEY_PATTERN = /accessKey|secretKey|password|secret|token|jwt/i;
3006
+ function redactSensitive(obj) {
3007
+ const result = {};
3008
+ for (const [key, value] of Object.entries(obj)) {
3009
+ if (SENSITIVE_KEY_PATTERN.test(key)) {
3010
+ result[key] = "***REDACTED***";
3011
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
3012
+ result[key] = redactSensitive(value);
3013
+ } else {
3014
+ result[key] = value;
3015
+ }
3016
+ }
3017
+ return result;
3018
+ }
3019
+ function todayDateString() {
3020
+ const d = /* @__PURE__ */ new Date();
3021
+ const yyyy = d.getFullYear();
3022
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
3023
+ const dd = String(d.getDate()).padStart(2, "0");
3024
+ return `${yyyy}-${mm}-${dd}`;
3025
+ }
3026
+ var TradeLogger = class {
3027
+ logDir;
3028
+ minLevel;
3029
+ verbose;
3030
+ constructor(minLevelOrOptions) {
3031
+ if (typeof minLevelOrOptions === "string") {
3032
+ this.logDir = join3(homedir3(), ".bithumb", "logs");
3033
+ this.minLevel = minLevelOrOptions;
3034
+ this.verbose = false;
3035
+ } else {
3036
+ this.logDir = minLevelOrOptions?.logDir ?? join3(homedir3(), ".bithumb", "logs");
3037
+ this.minLevel = minLevelOrOptions?.minLevel ?? "info";
3038
+ this.verbose = minLevelOrOptions?.verbose ?? false;
3039
+ }
3040
+ try {
3041
+ mkdirSync2(this.logDir, { recursive: true });
3042
+ } catch {
3043
+ }
3044
+ }
3045
+ debug(message, meta) {
3046
+ this.log("debug", message, meta);
3047
+ }
3048
+ info(message, meta) {
3049
+ this.log("info", message, meta);
3050
+ }
3051
+ warn(message, meta) {
3052
+ this.log("warn", message, meta);
3053
+ }
3054
+ error(message, meta) {
3055
+ this.log("error", message, meta);
3056
+ }
3057
+ /** Log a tool invocation result (used by MCP server). */
3058
+ logTool(level, toolName, args, result, elapsedMs) {
3059
+ this.log(level, `tool:${toolName}`, {
3060
+ args,
3061
+ result,
3062
+ elapsedMs
3063
+ });
3064
+ }
3065
+ log(level, message, meta) {
3066
+ if (LEVEL_ORDER[level] < LEVEL_ORDER[this.minLevel]) return;
3067
+ const entry = {
3068
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
3069
+ level,
3070
+ message,
3071
+ ...meta ? redactSensitive(meta) : {}
3072
+ };
3073
+ const line = JSON.stringify(entry);
3074
+ if (this.verbose || level === "error") {
3075
+ process.stderr.write(`[${level}] ${message}
3076
+ `);
3077
+ }
3078
+ try {
3079
+ const filePath = join3(this.logDir, `trade-${todayDateString()}.log`);
3080
+ appendFileSync(filePath, line + "\n", "utf8");
3081
+ } catch {
3082
+ }
3083
+ }
3084
+ };
3085
+ var CACHE_DIR = join4(homedir4(), ".bithumb");
3086
+ var CACHE_FILE = join4(CACHE_DIR, "update-check.json");
3087
+ var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
3088
+ function isNewerVersion(current, latest) {
3089
+ const parse2 = (v) => v.replace(/^v/, "").split(".").map((n) => parseInt(n, 10) || 0);
3090
+ const cur = parse2(current);
3091
+ const lat = parse2(latest);
3092
+ for (let i = 0; i < Math.max(cur.length, lat.length); i++) {
3093
+ const c = cur[i] ?? 0;
3094
+ const l = lat[i] ?? 0;
3095
+ if (l > c) return true;
3096
+ if (l < c) return false;
3097
+ }
3098
+ return false;
3099
+ }
3100
+ async function fetchDistTags(packageName) {
3101
+ try {
3102
+ const url = `https://registry.npmjs.org/-/package/${encodeURIComponent(packageName)}/dist-tags`;
3103
+ const res = await fetch(url, { signal: AbortSignal.timeout(5e3) });
3104
+ if (!res.ok) return null;
3105
+ return await res.json();
3106
+ } catch {
3107
+ return null;
3108
+ }
3109
+ }
3110
+ async function fetchLatestVersion(packageName) {
3111
+ const tags = await fetchDistTags(packageName);
3112
+ return tags?.latest ?? null;
3113
+ }
3114
+ function readCache() {
3115
+ try {
3116
+ if (!existsSync3(CACHE_FILE)) return null;
3117
+ const raw = readFileSync2(CACHE_FILE, "utf8");
3118
+ return JSON.parse(raw);
3119
+ } catch {
3120
+ return null;
3121
+ }
3122
+ }
3123
+ function writeCache(data) {
3124
+ try {
3125
+ if (!existsSync3(CACHE_DIR)) {
3126
+ mkdirSync3(CACHE_DIR, { recursive: true });
3127
+ }
3128
+ writeFileSync2(CACHE_FILE, JSON.stringify(data), "utf8");
3129
+ } catch {
3130
+ }
3131
+ }
3132
+ function isCacheStale(cache) {
3133
+ return Date.now() - cache.checkedAt > CACHE_TTL_MS;
3134
+ }
3135
+ function checkForUpdates(packageName, currentVersion) {
3136
+ try {
3137
+ const cache = readCache();
3138
+ if (cache && !isCacheStale(cache)) {
3139
+ if (isNewerVersion(currentVersion, cache.latestVersion)) {
3140
+ process.stderr.write(
3141
+ `
3142
+ Update available: ${currentVersion} \u2192 ${cache.latestVersion}
3143
+ Run: npm install -g ${packageName}
3144
+
3145
+ `
3146
+ );
3147
+ }
3148
+ return;
3149
+ }
3150
+ setImmediate(() => {
3151
+ fetchLatestVersion(packageName).then((latest) => {
3152
+ if (latest) {
3153
+ writeCache({ latestVersion: latest, checkedAt: Date.now() });
3154
+ if (isNewerVersion(currentVersion, latest)) {
3155
+ process.stderr.write(
3156
+ `
3157
+ Update available: ${currentVersion} \u2192 ${latest}
3158
+ Run: npm install -g ${packageName}
3159
+
3160
+ `
3161
+ );
3162
+ }
3163
+ }
3164
+ }).catch(() => {
3165
+ });
3166
+ });
3167
+ } catch {
3168
+ }
3169
+ }
3170
+ var CLIENT_NAMES = {
3171
+ "claude-desktop": "Claude Desktop",
3172
+ cursor: "Cursor",
3173
+ windsurf: "Windsurf",
3174
+ vscode: "VS Code",
3175
+ "claude-code": "Claude Code CLI"
3176
+ };
3177
+ var SUPPORTED_CLIENTS = Object.keys(CLIENT_NAMES);
3178
+ function appData() {
3179
+ return process.env.APPDATA ?? path2.join(os2.homedir(), "AppData", "Roaming");
3180
+ }
3181
+ var CLAUDE_CONFIG_FILE = "claude_desktop_config.json";
3182
+ function findMsStoreClaudePath() {
3183
+ const localAppData = process.env.LOCALAPPDATA ?? path2.join(os2.homedir(), "AppData", "Local");
3184
+ const packagesDir = path2.join(localAppData, "Packages");
3185
+ try {
3186
+ const entries = fs2.readdirSync(packagesDir);
3187
+ const claudePkg = entries.find((e) => e.startsWith("Claude_"));
3188
+ if (claudePkg) {
3189
+ const configPath = path2.join(
3190
+ packagesDir,
3191
+ claudePkg,
3192
+ "LocalCache",
3193
+ "Roaming",
3194
+ "Claude",
3195
+ CLAUDE_CONFIG_FILE
3196
+ );
3197
+ if (fs2.existsSync(configPath) || fs2.existsSync(path2.dirname(configPath))) {
3198
+ return configPath;
3199
+ }
3200
+ }
3201
+ } catch {
3202
+ }
3203
+ return null;
3204
+ }
3205
+ function getConfigPath(client) {
3206
+ const home = os2.homedir();
3207
+ const platform = process.platform;
3208
+ switch (client) {
3209
+ case "claude-desktop":
3210
+ if (platform === "win32") {
3211
+ return findMsStoreClaudePath() ?? path2.join(appData(), "Claude", CLAUDE_CONFIG_FILE);
3212
+ }
3213
+ if (platform === "darwin") {
3214
+ return path2.join(home, "Library", "Application Support", "Claude", CLAUDE_CONFIG_FILE);
3215
+ }
3216
+ return path2.join(process.env.XDG_CONFIG_HOME ?? path2.join(home, ".config"), "Claude", CLAUDE_CONFIG_FILE);
3217
+ case "cursor":
3218
+ return path2.join(home, ".cursor", "mcp.json");
3219
+ case "windsurf":
3220
+ return path2.join(home, ".codeium", "windsurf", "mcp_config.json");
3221
+ case "vscode":
3222
+ return path2.join(process.cwd(), ".mcp.json");
3223
+ case "claude-code":
3224
+ return null;
3225
+ }
3226
+ }
3227
+ var NPX_PACKAGE = "@bithumb-official/bithumb-mcp";
3228
+ function buildEntry(client, args) {
3229
+ if (client === "vscode") {
3230
+ return { type: "stdio", command: "bithumb-mcp", args };
3231
+ }
3232
+ return { command: "npx", args: ["-y", NPX_PACKAGE, ...args] };
3233
+ }
3234
+ function buildArgs(options) {
3235
+ const args = [];
3236
+ if (options.profile) args.push("--profile", options.profile);
3237
+ args.push("--modules", options.modules ?? "all");
3238
+ return args;
3239
+ }
3240
+ function mergeJsonConfig(configPath, serverName, entry) {
3241
+ const dir = path2.dirname(configPath);
3242
+ if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
3243
+ let data = {};
3244
+ if (fs2.existsSync(configPath)) {
3245
+ const raw = fs2.readFileSync(configPath, "utf-8");
3246
+ try {
3247
+ data = JSON.parse(raw);
3248
+ } catch {
3249
+ throw new Error(`Failed to parse existing config at ${configPath}`);
3250
+ }
3251
+ const backupPath = configPath + ".bak";
3252
+ fs2.copyFileSync(configPath, backupPath);
3253
+ process.stdout.write(` Backup \u2192 ${backupPath}
3254
+ `);
3255
+ }
3256
+ if (typeof data.mcpServers !== "object" || data.mcpServers === null) {
3257
+ data.mcpServers = {};
3258
+ }
3259
+ data.mcpServers[serverName] = entry;
3260
+ fs2.writeFileSync(configPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
3261
+ }
3262
+ function printSetupUsage() {
3263
+ process.stdout.write(
3264
+ `Usage: bithumb setup --client <client> [--profile <name>] [--modules <list>]
3265
+
3266
+ Clients:
3267
+ ` + SUPPORTED_CLIENTS.map((id) => ` ${id.padEnd(16)} ${CLIENT_NAMES[id]}`).join("\n") + `
3268
+
3269
+ Options:
3270
+ --profile <name> Profile from ${configFilePath()} (default: uses default_profile)
3271
+ --modules <list> Comma-separated modules or "all" (default: all)
3272
+ `
3273
+ );
3274
+ }
3275
+ function runSetup(options) {
3276
+ const { client } = options;
3277
+ const name = CLIENT_NAMES[client];
3278
+ const args = buildArgs(options);
3279
+ const serverName = options.profile ? `bithumb-mcp-${options.profile}` : "bithumb-mcp";
3280
+ if (client === "claude-code") {
3281
+ const claudeArgs = [
3282
+ "mcp",
3283
+ "add",
3284
+ "--transport",
3285
+ "stdio",
3286
+ serverName,
3287
+ "--",
3288
+ "bithumb-mcp",
3289
+ ...args
3290
+ ];
3291
+ process.stdout.write(`Running: claude ${claudeArgs.join(" ")}
3292
+ `);
3293
+ execFileSync("claude", claudeArgs, { stdio: "inherit" });
3294
+ process.stdout.write(`\u2713 Configured ${name}
3295
+ `);
3296
+ return;
3297
+ }
3298
+ const configPath = getConfigPath(client);
3299
+ if (!configPath) {
3300
+ throw new Error(`${name} is not supported on this platform`);
3301
+ }
3302
+ const entry = buildEntry(client, args);
3303
+ mergeJsonConfig(configPath, serverName, entry);
3304
+ process.stdout.write(
3305
+ `\u2713 Configured ${name}
3306
+ ${configPath}
3307
+ Server args: ${args.join(" ")}
3308
+ `
3309
+ );
3310
+ if (client !== "vscode") {
3311
+ process.stdout.write(` Restart ${name} to apply changes.
3312
+ `);
3313
+ }
3314
+ }
3315
+
3316
+ // src/constants.ts
3317
+ import { createRequire } from "module";
3318
+ var _require = createRequire(import.meta.url);
3319
+ var pkg = _require("../package.json");
3320
+ var SERVER_NAME = "bithumb-mcp";
3321
+ var SERVER_VERSION = pkg.version;
3322
+
3323
+ // src/server.ts
3324
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3325
+ import {
3326
+ CallToolRequestSchema,
3327
+ ListToolsRequestSchema
3328
+ } from "@modelcontextprotocol/sdk/types.js";
3329
+ var SYSTEM_CAPABILITIES_TOOL_NAME = "system_get_capabilities";
3330
+ var SYSTEM_CAPABILITIES_TOOL = {
3331
+ name: SYSTEM_CAPABILITIES_TOOL_NAME,
3332
+ description: "Return server capabilities and module availability for agent planning. Use when: user asks '\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uBE57\uC378 \uAE30\uB2A5 \uC54C\uB824\uC918' / 'what can you do?' / 'what features are available?'; or when an agent needs to plan a multi-step workflow and must know which modules are enabled. Do NOT use: as a substitute for `system_diagnose` (which checks live connectivity and auth). Returns: server name/version, readOnly flag, hasAuth flag, and per-module status (enabled / disabled / requires_auth).",
3333
+ inputSchema: { type: "object", additionalProperties: false },
3334
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }
3335
+ };
3336
+ function buildCapabilitySnapshot(config) {
3337
+ const enabledModules = new Set(config.modules);
3338
+ const moduleAvailability = {};
3339
+ for (const moduleId of MODULES) {
3340
+ if (!enabledModules.has(moduleId)) {
3341
+ moduleAvailability[moduleId] = { status: "disabled", reasonCode: "MODULE_FILTERED" };
3342
+ continue;
3343
+ }
3344
+ if (moduleId === "market" || moduleId === "system") {
3345
+ moduleAvailability[moduleId] = { status: "enabled" };
3346
+ continue;
3347
+ }
3348
+ if (!config.hasAuth) {
3349
+ moduleAvailability[moduleId] = { status: "requires_auth", reasonCode: "AUTH_MISSING" };
3350
+ continue;
3351
+ }
3352
+ moduleAvailability[moduleId] = { status: "enabled" };
3353
+ }
3354
+ return { readOnly: config.readOnly, hasAuth: config.hasAuth, moduleAvailability };
3355
+ }
3356
+ function successResult(toolName, data, snapshot) {
3357
+ const payload = {
3358
+ tool: toolName,
3359
+ ok: true,
3360
+ data,
3361
+ capabilities: snapshot,
3362
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3363
+ };
3364
+ return {
3365
+ content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
3366
+ structuredContent: payload
3367
+ };
3368
+ }
3369
+ function errorResult(toolName, error, snapshot) {
3370
+ const payload = toToolErrorPayload(error);
3371
+ const structured = {
3372
+ tool: toolName,
3373
+ ...payload,
3374
+ serverVersion: SERVER_VERSION,
3375
+ capabilities: snapshot
3376
+ };
3377
+ return {
3378
+ isError: true,
3379
+ content: [{ type: "text", text: JSON.stringify(structured, null, 2) }],
3380
+ structuredContent: structured
3381
+ };
3382
+ }
3383
+ function unknownToolResult(toolName, snapshot) {
3384
+ return errorResult(
3385
+ toolName,
3386
+ new BithumbApiError(`Tool "${toolName}" is not available.`, { code: "TOOL_NOT_AVAILABLE" }),
3387
+ snapshot
3388
+ );
3389
+ }
3390
+ function createServer(config, logger) {
3391
+ const client = new BithumbRestClient(config);
3392
+ const tools = buildTools(config);
3393
+ const toolMap = new Map(tools.map((t) => [t.name, t]));
3394
+ const server = new Server(
3395
+ { name: SERVER_NAME, version: SERVER_VERSION },
3396
+ { capabilities: { tools: {} } }
3397
+ );
3398
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
3399
+ const coreToolNames = new Set(tools.map((t) => t.name));
3400
+ const extraTools = coreToolNames.has(SYSTEM_CAPABILITIES_TOOL_NAME) ? [] : [SYSTEM_CAPABILITIES_TOOL];
3401
+ return { tools: [...tools.map(toMcpTool), ...extraTools] };
3402
+ });
3403
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3404
+ const toolName = request.params.name;
3405
+ if (toolName === SYSTEM_CAPABILITIES_TOOL_NAME) {
3406
+ const snapshot = buildCapabilitySnapshot(config);
3407
+ return successResult(toolName, { server: { name: SERVER_NAME, version: SERVER_VERSION }, capabilities: snapshot }, snapshot);
3408
+ }
3409
+ const tool = toolMap.get(toolName);
3410
+ if (!tool) return unknownToolResult(toolName, buildCapabilitySnapshot(config));
3411
+ const startTime = Date.now();
3412
+ try {
3413
+ const response = await tool.handler(request.params.arguments ?? {}, { config, client });
3414
+ logger?.logTool("info", toolName, request.params.arguments ?? {}, response, Date.now() - startTime);
3415
+ return successResult(toolName, response, buildCapabilitySnapshot(config));
3416
+ } catch (error) {
3417
+ const level = error instanceof BithumbApiError ? "warn" : "error";
3418
+ logger?.logTool(level, toolName, request.params.arguments ?? {}, error, Date.now() - startTime);
3419
+ return errorResult(toolName, error, buildCapabilitySnapshot(config));
3420
+ }
3421
+ });
3422
+ return server;
3423
+ }
3424
+
3425
+ // src/index.ts
3426
+ function printHelp() {
3427
+ const help = `
3428
+ Usage: bithumb-mcp [options]
3429
+
3430
+ Options:
3431
+ --modules <list> Comma-separated modules: market, account, trade, twap, withdraw, deposit, or "all"
3432
+ Default: market,account,trade,twap,withdraw,deposit (all modules)
3433
+ --read-only Expose only read/query tools (disable order placement/cancellation)
3434
+ --no-log Disable audit logging
3435
+ --log-level <level> Log level: error, warn, info, debug (default: info)
3436
+ --verbose Enable verbose request logging to stderr
3437
+ --profile <name> Config profile from ~/.bithumb/config.toml (default: uses default_profile)
3438
+ --help Show this help message
3439
+ --version Show version
3440
+
3441
+ Credentials (environment variables):
3442
+ BITHUMB_ACCESS_KEY Bithumb API access key
3443
+ BITHUMB_SECRET_KEY Bithumb API secret key
3444
+
3445
+ Other Environment Variables:
3446
+ BITHUMB_API_BASE_URL Optional API base URL override (default: https://api.bithumb.com)
3447
+ BITHUMB_TIMEOUT_MS Request timeout in milliseconds (default: 15000)
3448
+ `;
3449
+ process.stdout.write(help);
3450
+ }
3451
+ function parseCli() {
3452
+ const parsed = parseArgs({
3453
+ options: {
3454
+ modules: { type: "string" },
3455
+ "read-only": { type: "boolean", default: false },
3456
+ "no-log": { type: "boolean", default: false },
3457
+ "log-level": { type: "string", default: "info" },
3458
+ verbose: { type: "boolean", default: false },
3459
+ profile: { type: "string" },
3460
+ help: { type: "boolean", default: false },
3461
+ version: { type: "boolean", default: false }
3462
+ },
3463
+ allowPositionals: false
3464
+ });
3465
+ return {
3466
+ modules: parsed.values.modules,
3467
+ readOnly: parsed.values["read-only"] ?? false,
3468
+ noLog: parsed.values["no-log"] ?? false,
3469
+ logLevel: parsed.values["log-level"] ?? "info",
3470
+ verbose: parsed.values.verbose ?? false,
3471
+ profile: parsed.values.profile,
3472
+ help: parsed.values.help ?? false,
3473
+ version: parsed.values.version ?? false
3474
+ };
3475
+ }
3476
+ async function main() {
3477
+ if (process.argv[2] === "setup") {
3478
+ const setupArgs = parseArgs({
3479
+ args: process.argv.slice(3),
3480
+ options: {
3481
+ client: { type: "string" },
3482
+ profile: { type: "string" },
3483
+ modules: { type: "string" },
3484
+ help: { type: "boolean", default: false }
3485
+ },
3486
+ allowPositionals: false
3487
+ });
3488
+ if (setupArgs.values.help || !setupArgs.values.client) {
3489
+ printSetupUsage();
3490
+ return;
3491
+ }
3492
+ if (!SUPPORTED_CLIENTS.includes(setupArgs.values.client)) {
3493
+ process.stderr.write(`Unknown client "${setupArgs.values.client}". Use: ${SUPPORTED_CLIENTS.join(", ")}
3494
+ `);
3495
+ process.exitCode = 1;
3496
+ return;
3497
+ }
3498
+ runSetup({
3499
+ client: setupArgs.values.client,
3500
+ profile: setupArgs.values.profile,
3501
+ modules: setupArgs.values.modules
3502
+ });
3503
+ return;
3504
+ }
3505
+ const cli = parseCli();
3506
+ if (cli.help) {
3507
+ printHelp();
3508
+ return;
3509
+ }
3510
+ if (cli.version) {
3511
+ process.stdout.write(`${SERVER_VERSION}
3512
+ `);
3513
+ return;
3514
+ }
3515
+ checkForUpdates("@bithumb-official/bithumb-mcp", SERVER_VERSION);
3516
+ const config = loadConfig({
3517
+ modules: cli.modules,
3518
+ readOnly: cli.readOnly,
3519
+ verbose: cli.verbose,
3520
+ profile: cli.profile,
3521
+ clientType: "mcp"
3522
+ });
3523
+ const logger = cli.noLog ? void 0 : new TradeLogger(cli.logLevel);
3524
+ const server = createServer(config, logger);
3525
+ const transport = new StdioServerTransport();
3526
+ await server.connect(transport);
3527
+ }
3528
+ main().catch((error) => {
3529
+ const payload = toToolErrorPayload(error);
3530
+ process.stderr.write(`${JSON.stringify(payload, null, 2)}
3531
+ `);
3532
+ process.exitCode = 1;
3533
+ });
3534
+ export {
3535
+ main
3536
+ };
3537
+ /*! Bundled license information:
3538
+
3539
+ smol-toml/dist/error.js:
3540
+ smol-toml/dist/util.js:
3541
+ smol-toml/dist/date.js:
3542
+ smol-toml/dist/primitive.js:
3543
+ smol-toml/dist/extract.js:
3544
+ smol-toml/dist/struct.js:
3545
+ smol-toml/dist/parse.js:
3546
+ smol-toml/dist/stringify.js:
3547
+ smol-toml/dist/index.js:
3548
+ (*!
3549
+ * Copyright (c) Squirrel Chat et al., All rights reserved.
3550
+ * SPDX-License-Identifier: BSD-3-Clause
3551
+ *
3552
+ * Redistribution and use in source and binary forms, with or without
3553
+ * modification, are permitted provided that the following conditions are met:
3554
+ *
3555
+ * 1. Redistributions of source code must retain the above copyright notice, this
3556
+ * list of conditions and the following disclaimer.
3557
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
3558
+ * this list of conditions and the following disclaimer in the
3559
+ * documentation and/or other materials provided with the distribution.
3560
+ * 3. Neither the name of the copyright holder nor the names of its contributors
3561
+ * may be used to endorse or promote products derived from this software without
3562
+ * specific prior written permission.
3563
+ *
3564
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
3565
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
3566
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
3567
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
3568
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
3569
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
3570
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
3571
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
3572
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
3573
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
3574
+ *)
3575
+ */
3576
+ //# sourceMappingURL=index.js.map