krx-cli 1.3.0 → 1.5.0

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/mcp.js CHANGED
@@ -6,9 +6,6 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __esm = (fn, res) => function __init() {
10
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
- };
12
9
  var __commonJS = (cb, mod) => function __require() {
13
10
  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
14
11
  };
@@ -2984,7 +2981,7 @@ var require_compile = __commonJS({
2984
2981
  const schOrFunc = root.refs[ref];
2985
2982
  if (schOrFunc)
2986
2983
  return schOrFunc;
2987
- let _sch = resolve.call(this, root, ref);
2984
+ let _sch = resolve2.call(this, root, ref);
2988
2985
  if (_sch === void 0) {
2989
2986
  const schema = (_a2 = root.localRefs) === null || _a2 === void 0 ? void 0 : _a2[ref];
2990
2987
  const { schemaId } = this.opts;
@@ -3011,7 +3008,7 @@ var require_compile = __commonJS({
3011
3008
  function sameSchemaEnv(s1, s2) {
3012
3009
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3013
3010
  }
3014
- function resolve(root, ref) {
3011
+ function resolve2(root, ref) {
3015
3012
  let sch;
3016
3013
  while (typeof (sch = this.refs[ref]) == "string")
3017
3014
  ref = sch;
@@ -3226,8 +3223,8 @@ var require_utils = __commonJS({
3226
3223
  }
3227
3224
  return ind;
3228
3225
  }
3229
- function removeDotSegments(path3) {
3230
- let input = path3;
3226
+ function removeDotSegments(path5) {
3227
+ let input = path5;
3231
3228
  const output = [];
3232
3229
  let nextSlash = -1;
3233
3230
  let len = 0;
@@ -3426,8 +3423,8 @@ var require_schemes = __commonJS({
3426
3423
  wsComponent.secure = void 0;
3427
3424
  }
3428
3425
  if (wsComponent.resourceName) {
3429
- const [path3, query] = wsComponent.resourceName.split("?");
3430
- wsComponent.path = path3 && path3 !== "/" ? path3 : void 0;
3426
+ const [path5, query] = wsComponent.resourceName.split("?");
3427
+ wsComponent.path = path5 && path5 !== "/" ? path5 : void 0;
3431
3428
  wsComponent.query = query;
3432
3429
  wsComponent.resourceName = void 0;
3433
3430
  }
@@ -3586,7 +3583,7 @@ var require_fast_uri = __commonJS({
3586
3583
  }
3587
3584
  return uri;
3588
3585
  }
3589
- function resolve(baseURI, relativeURI, options) {
3586
+ function resolve2(baseURI, relativeURI, options) {
3590
3587
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3591
3588
  const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
3592
3589
  schemelessOptions.skipEscape = true;
@@ -3813,7 +3810,7 @@ var require_fast_uri = __commonJS({
3813
3810
  var fastUri = {
3814
3811
  SCHEMES,
3815
3812
  normalize,
3816
- resolve,
3813
+ resolve: resolve2,
3817
3814
  resolveComponent,
3818
3815
  equal,
3819
3816
  serialize,
@@ -6789,12 +6786,12 @@ var require_dist = __commonJS({
6789
6786
  throw new Error(`Unknown format "${name}"`);
6790
6787
  return f;
6791
6788
  };
6792
- function addFormats(ajv, list, fs3, exportName) {
6789
+ function addFormats(ajv, list, fs5, exportName) {
6793
6790
  var _a2;
6794
6791
  var _b;
6795
6792
  (_a2 = (_b = ajv.opts.code).formats) !== null && _a2 !== void 0 ? _a2 : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
6796
6793
  for (const f of list)
6797
- ajv.addFormat(f, fs3[f]);
6794
+ ajv.addFormat(f, fs5[f]);
6798
6795
  }
6799
6796
  module.exports = exports = formatsPlugin;
6800
6797
  Object.defineProperty(exports, "__esModule", { value: true });
@@ -6802,72 +6799,6 @@ var require_dist = __commonJS({
6802
6799
  }
6803
6800
  });
6804
6801
 
6805
- // src/client/rate-limit.ts
6806
- var rate_limit_exports = {};
6807
- __export(rate_limit_exports, {
6808
- checkRateLimit: () => checkRateLimit,
6809
- getRateLimitStatus: () => getRateLimitStatus,
6810
- incrementCallCount: () => incrementCallCount
6811
- });
6812
- import * as fs from "node:fs";
6813
- import * as path from "node:path";
6814
- import * as os from "node:os";
6815
- function today() {
6816
- const now = /* @__PURE__ */ new Date();
6817
- const yyyy = now.getFullYear().toString();
6818
- const mm = (now.getMonth() + 1).toString().padStart(2, "0");
6819
- const dd = now.getDate().toString().padStart(2, "0");
6820
- return `${yyyy}${mm}${dd}`;
6821
- }
6822
- function readRateData() {
6823
- try {
6824
- const raw = fs.readFileSync(RATE_FILE, "utf-8");
6825
- return JSON.parse(raw);
6826
- } catch {
6827
- return { date: today(), count: 0 };
6828
- }
6829
- }
6830
- function writeRateData(data) {
6831
- fs.mkdirSync(CONFIG_DIR, { recursive: true });
6832
- fs.writeFileSync(RATE_FILE, JSON.stringify(data, null, 2), "utf-8");
6833
- }
6834
- function incrementCallCount() {
6835
- const data = readRateData();
6836
- const currentDate = today();
6837
- const newData = data.date === currentDate ? { date: currentDate, count: data.count + 1 } : { date: currentDate, count: 1 };
6838
- writeRateData(newData);
6839
- return { count: newData.count, limit: DAILY_LIMIT };
6840
- }
6841
- function checkRateLimit() {
6842
- const data = readRateData();
6843
- const currentDate = today();
6844
- const count = data.date === currentDate ? data.count : 0;
6845
- const allowed = count < DAILY_LIMIT;
6846
- const warning = count >= DAILY_LIMIT * WARNING_THRESHOLD;
6847
- return { allowed, count, limit: DAILY_LIMIT, warning };
6848
- }
6849
- function getRateLimitStatus() {
6850
- const data = readRateData();
6851
- const currentDate = today();
6852
- const count = data.date === currentDate ? data.count : 0;
6853
- return {
6854
- date: currentDate,
6855
- count,
6856
- limit: DAILY_LIMIT,
6857
- remaining: DAILY_LIMIT - count
6858
- };
6859
- }
6860
- var CONFIG_DIR, RATE_FILE, DAILY_LIMIT, WARNING_THRESHOLD;
6861
- var init_rate_limit = __esm({
6862
- "src/client/rate-limit.ts"() {
6863
- "use strict";
6864
- CONFIG_DIR = path.join(os.homedir(), ".krx-cli");
6865
- RATE_FILE = path.join(CONFIG_DIR, "rate-limit.json");
6866
- DAILY_LIMIT = 1e4;
6867
- WARNING_THRESHOLD = 0.8;
6868
- }
6869
- });
6870
-
6871
6802
  // node_modules/.pnpm/@modelcontextprotocol+sdk@1.27.1_zod@4.3.6/node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
6872
6803
  import process3 from "node:process";
6873
6804
 
@@ -7638,10 +7569,10 @@ function mergeDefs(...defs) {
7638
7569
  function cloneDef(schema) {
7639
7570
  return mergeDefs(schema._zod.def);
7640
7571
  }
7641
- function getElementAtPath(obj, path3) {
7642
- if (!path3)
7572
+ function getElementAtPath(obj, path5) {
7573
+ if (!path5)
7643
7574
  return obj;
7644
- return path3.reduce((acc, key) => acc?.[key], obj);
7575
+ return path5.reduce((acc, key) => acc?.[key], obj);
7645
7576
  }
7646
7577
  function promiseAllObject(promisesObj) {
7647
7578
  const keys = Object.keys(promisesObj);
@@ -8024,11 +7955,11 @@ function aborted(x, startIndex = 0) {
8024
7955
  }
8025
7956
  return false;
8026
7957
  }
8027
- function prefixIssues(path3, issues) {
7958
+ function prefixIssues(path5, issues) {
8028
7959
  return issues.map((iss) => {
8029
7960
  var _a2;
8030
7961
  (_a2 = iss).path ?? (_a2.path = []);
8031
- iss.path.unshift(path3);
7962
+ iss.path.unshift(path5);
8032
7963
  return iss;
8033
7964
  });
8034
7965
  }
@@ -8211,7 +8142,7 @@ function formatError(error48, mapper = (issue2) => issue2.message) {
8211
8142
  }
8212
8143
  function treeifyError(error48, mapper = (issue2) => issue2.message) {
8213
8144
  const result = { errors: [] };
8214
- const processError = (error49, path3 = []) => {
8145
+ const processError = (error49, path5 = []) => {
8215
8146
  var _a2, _b;
8216
8147
  for (const issue2 of error49.issues) {
8217
8148
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -8221,7 +8152,7 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
8221
8152
  } else if (issue2.code === "invalid_element") {
8222
8153
  processError({ issues: issue2.issues }, issue2.path);
8223
8154
  } else {
8224
- const fullpath = [...path3, ...issue2.path];
8155
+ const fullpath = [...path5, ...issue2.path];
8225
8156
  if (fullpath.length === 0) {
8226
8157
  result.errors.push(mapper(issue2));
8227
8158
  continue;
@@ -8253,8 +8184,8 @@ function treeifyError(error48, mapper = (issue2) => issue2.message) {
8253
8184
  }
8254
8185
  function toDotPath(_path) {
8255
8186
  const segs = [];
8256
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
8257
- for (const seg of path3) {
8187
+ const path5 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
8188
+ for (const seg of path5) {
8258
8189
  if (typeof seg === "number")
8259
8190
  segs.push(`[${seg}]`);
8260
8191
  else if (typeof seg === "symbol")
@@ -20231,13 +20162,13 @@ function resolveRef(ref, ctx) {
20231
20162
  if (!ref.startsWith("#")) {
20232
20163
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
20233
20164
  }
20234
- const path3 = ref.slice(1).split("/").filter(Boolean);
20235
- if (path3.length === 0) {
20165
+ const path5 = ref.slice(1).split("/").filter(Boolean);
20166
+ if (path5.length === 0) {
20236
20167
  return ctx.rootSchema;
20237
20168
  }
20238
20169
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
20239
- if (path3[0] === defsKey) {
20240
- const key = path3[1];
20170
+ if (path5[0] === defsKey) {
20171
+ const key = path5[1];
20241
20172
  if (!key || !ctx.defs[key]) {
20242
20173
  throw new Error(`Reference not found: ${ref}`);
20243
20174
  }
@@ -22235,12 +22166,12 @@ var StdioServerTransport = class {
22235
22166
  this.onclose?.();
22236
22167
  }
22237
22168
  send(message) {
22238
- return new Promise((resolve) => {
22169
+ return new Promise((resolve2) => {
22239
22170
  const json2 = serializeMessage(message);
22240
22171
  if (this._stdout.write(json2)) {
22241
- resolve();
22172
+ resolve2();
22242
22173
  } else {
22243
- this._stdout.once("drain", resolve);
22174
+ this._stdout.once("drain", resolve2);
22244
22175
  }
22245
22176
  });
22246
22177
  }
@@ -22605,8 +22536,8 @@ function getErrorMap2() {
22605
22536
 
22606
22537
  // node_modules/.pnpm/zod@4.3.6/node_modules/zod/v3/helpers/parseUtil.js
22607
22538
  var makeIssue = (params) => {
22608
- const { data, path: path3, errorMaps, issueData } = params;
22609
- const fullPath = [...path3, ...issueData.path || []];
22539
+ const { data, path: path5, errorMaps, issueData } = params;
22540
+ const fullPath = [...path5, ...issueData.path || []];
22610
22541
  const fullIssue = {
22611
22542
  ...issueData,
22612
22543
  path: fullPath
@@ -22721,11 +22652,11 @@ var errorUtil;
22721
22652
 
22722
22653
  // node_modules/.pnpm/zod@4.3.6/node_modules/zod/v3/types.js
22723
22654
  var ParseInputLazyPath = class {
22724
- constructor(parent, value, path3, key) {
22655
+ constructor(parent, value, path5, key) {
22725
22656
  this._cachedPath = [];
22726
22657
  this.parent = parent;
22727
22658
  this.data = value;
22728
- this._path = path3;
22659
+ this._path = path5;
22729
22660
  this._key = key;
22730
22661
  }
22731
22662
  get path() {
@@ -28135,7 +28066,7 @@ var Protocol = class {
28135
28066
  return;
28136
28067
  }
28137
28068
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
28138
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
28069
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
28139
28070
  options?.signal?.throwIfAborted();
28140
28071
  }
28141
28072
  } catch (error48) {
@@ -28152,7 +28083,7 @@ var Protocol = class {
28152
28083
  */
28153
28084
  request(request, resultSchema, options) {
28154
28085
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
28155
- return new Promise((resolve, reject) => {
28086
+ return new Promise((resolve2, reject) => {
28156
28087
  const earlyReject = (error48) => {
28157
28088
  reject(error48);
28158
28089
  };
@@ -28230,7 +28161,7 @@ var Protocol = class {
28230
28161
  if (!parseResult.success) {
28231
28162
  reject(parseResult.error);
28232
28163
  } else {
28233
- resolve(parseResult.data);
28164
+ resolve2(parseResult.data);
28234
28165
  }
28235
28166
  } catch (error48) {
28236
28167
  reject(error48);
@@ -28491,12 +28422,12 @@ var Protocol = class {
28491
28422
  }
28492
28423
  } catch {
28493
28424
  }
28494
- return new Promise((resolve, reject) => {
28425
+ return new Promise((resolve2, reject) => {
28495
28426
  if (signal.aborted) {
28496
28427
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
28497
28428
  return;
28498
28429
  }
28499
- const timeoutId = setTimeout(resolve, interval);
28430
+ const timeoutId = setTimeout(resolve2, interval);
28500
28431
  signal.addEventListener("abort", () => {
28501
28432
  clearTimeout(timeoutId);
28502
28433
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -29596,7 +29527,7 @@ var McpServer = class {
29596
29527
  let task = createTaskResult.task;
29597
29528
  const pollInterval = task.pollInterval ?? 5e3;
29598
29529
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
29599
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
29530
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
29600
29531
  const updatedTask = await extra.taskStore.getTask(taskId);
29601
29532
  if (!updatedTask) {
29602
29533
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -30547,13 +30478,13 @@ var CATEGORIES = [
30547
30478
  probeEndpoint: "/svc/apis/esg/esg_index_info"
30548
30479
  }
30549
30480
  ];
30550
- function ep(path3, description, descriptionKo, category) {
30481
+ function ep(path5, description, descriptionKo, category) {
30551
30482
  return {
30552
- path: path3,
30483
+ path: path5,
30553
30484
  description,
30554
30485
  descriptionKo,
30555
30486
  category,
30556
- responseFields: RESPONSE_FIELDS[path3] ?? []
30487
+ responseFields: RESPONSE_FIELDS[path5] ?? []
30557
30488
  };
30558
30489
  }
30559
30490
  var ENDPOINTS = [
@@ -30742,11 +30673,210 @@ var ENDPOINTS = [
30742
30673
  ep("/svc/apis/esg/esg_index_info", "ESG index info", "ESG \uC9C0\uC218 \uC815\uBCF4", "esg")
30743
30674
  ];
30744
30675
 
30676
+ // src/cache/store.ts
30677
+ import * as fs from "node:fs";
30678
+ import * as path from "node:path";
30679
+ import * as os from "node:os";
30680
+ import * as crypto from "node:crypto";
30681
+
30682
+ // src/utils/date.ts
30683
+ function formatDateToYYYYMMDD(date5) {
30684
+ const yyyy = date5.getFullYear().toString();
30685
+ const mm = (date5.getMonth() + 1).toString().padStart(2, "0");
30686
+ const dd = date5.getDate().toString().padStart(2, "0");
30687
+ return `${yyyy}${mm}${dd}`;
30688
+ }
30689
+ function isWeekend(date5) {
30690
+ const day = date5.getDay();
30691
+ return day === 0 || day === 6;
30692
+ }
30693
+ var KST_OFFSET_MS = 9 * 60 * 60 * 1e3;
30694
+ function getRecentTradingDate() {
30695
+ const nowKst = new Date(Date.now() + KST_OFFSET_MS);
30696
+ const day = nowKst.getUTCDay();
30697
+ const daysBack = day === 0 ? 2 : day === 6 ? 1 : day === 1 ? 3 : 1;
30698
+ const targetKst = new Date(nowKst.getTime() - daysBack * 24 * 60 * 60 * 1e3);
30699
+ const yyyy = targetKst.getUTCFullYear().toString();
30700
+ const mm = (targetKst.getUTCMonth() + 1).toString().padStart(2, "0");
30701
+ const dd = targetKst.getUTCDate().toString().padStart(2, "0");
30702
+ return `${yyyy}${mm}${dd}`;
30703
+ }
30704
+ function parseYYYYMMDD(str) {
30705
+ const year = parseInt(str.substring(0, 4));
30706
+ const month = parseInt(str.substring(4, 6)) - 1;
30707
+ const day = parseInt(str.substring(6, 8));
30708
+ return new Date(year, month, day);
30709
+ }
30710
+ function getTradingDays(from, to) {
30711
+ const fromDate = parseYYYYMMDD(from);
30712
+ const toDate = parseYYYYMMDD(to);
30713
+ const days = [];
30714
+ let current = new Date(fromDate);
30715
+ while (current <= toDate) {
30716
+ if (!isWeekend(current)) {
30717
+ days.push(formatDateToYYYYMMDD(current));
30718
+ }
30719
+ current = new Date(current.getTime() + 24 * 60 * 60 * 1e3);
30720
+ }
30721
+ return days;
30722
+ }
30723
+
30724
+ // src/cache/store.ts
30725
+ var CACHE_DIR = path.join(os.homedir(), ".krx-cli", "cache");
30726
+ var DATE_FORMAT = /^\d{8}$/;
30727
+ function isValidCacheDate(date5) {
30728
+ return DATE_FORMAT.test(date5);
30729
+ }
30730
+ function getCacheKey(endpoint, params) {
30731
+ const sortedEntries = Object.entries(params).sort(
30732
+ ([a], [b]) => a.localeCompare(b)
30733
+ );
30734
+ const raw = `${endpoint}:${JSON.stringify(sortedEntries)}`;
30735
+ return crypto.createHash("sha256").update(raw).digest("hex").slice(0, 16);
30736
+ }
30737
+ function getCachePath(date5, key) {
30738
+ return path.join(CACHE_DIR, date5, `${key}.json`);
30739
+ }
30740
+ function isToday(dateStr) {
30741
+ const today2 = formatDateToYYYYMMDD(/* @__PURE__ */ new Date());
30742
+ return dateStr === today2;
30743
+ }
30744
+ function getCached(endpoint, params) {
30745
+ const date5 = params["basDd"];
30746
+ if (!date5 || !isValidCacheDate(date5) || isToday(date5)) {
30747
+ return null;
30748
+ }
30749
+ const key = getCacheKey(endpoint, params);
30750
+ const cachePath = getCachePath(date5, key);
30751
+ try {
30752
+ const raw = fs.readFileSync(cachePath, "utf-8");
30753
+ return JSON.parse(raw);
30754
+ } catch (err) {
30755
+ if (err instanceof Error && err.message.includes("ENOENT")) {
30756
+ return null;
30757
+ }
30758
+ process.stderr.write(`[krx-cli] cache read failed: ${String(err)}
30759
+ `);
30760
+ return null;
30761
+ }
30762
+ }
30763
+ function setCached(endpoint, params, data) {
30764
+ const date5 = params["basDd"];
30765
+ if (!date5 || !isValidCacheDate(date5) || isToday(date5)) {
30766
+ return;
30767
+ }
30768
+ const key = getCacheKey(endpoint, params);
30769
+ const cachePath = getCachePath(date5, key);
30770
+ const dir = path.dirname(cachePath);
30771
+ try {
30772
+ fs.mkdirSync(dir, { recursive: true });
30773
+ fs.writeFileSync(cachePath, JSON.stringify(data), "utf-8");
30774
+ } catch (err) {
30775
+ process.stderr.write(`[krx-cli] cache write failed: ${String(err)}
30776
+ `);
30777
+ }
30778
+ }
30779
+
30780
+ // src/client/rate-limit.ts
30781
+ import * as fs2 from "node:fs";
30782
+ import * as path2 from "node:path";
30783
+ import * as os2 from "node:os";
30784
+ var CONFIG_DIR = path2.join(os2.homedir(), ".krx-cli");
30785
+ var RATE_FILE = path2.join(CONFIG_DIR, "rate-limit.json");
30786
+ var DAILY_LIMIT = 1e4;
30787
+ var WARNING_THRESHOLD = 0.8;
30788
+ function today() {
30789
+ const now = /* @__PURE__ */ new Date();
30790
+ const yyyy = now.getFullYear().toString();
30791
+ const mm = (now.getMonth() + 1).toString().padStart(2, "0");
30792
+ const dd = now.getDate().toString().padStart(2, "0");
30793
+ return `${yyyy}${mm}${dd}`;
30794
+ }
30795
+ function readRateData() {
30796
+ try {
30797
+ const raw = fs2.readFileSync(RATE_FILE, "utf-8");
30798
+ return JSON.parse(raw);
30799
+ } catch {
30800
+ return { date: today(), count: 0 };
30801
+ }
30802
+ }
30803
+ function writeRateData(data) {
30804
+ fs2.mkdirSync(CONFIG_DIR, { recursive: true });
30805
+ fs2.writeFileSync(RATE_FILE, JSON.stringify(data, null, 2), "utf-8");
30806
+ }
30807
+ function incrementCallCount() {
30808
+ const data = readRateData();
30809
+ const currentDate = today();
30810
+ const newData = data.date === currentDate ? { date: currentDate, count: data.count + 1 } : { date: currentDate, count: 1 };
30811
+ writeRateData(newData);
30812
+ return { count: newData.count, limit: DAILY_LIMIT };
30813
+ }
30814
+ function checkRateLimit() {
30815
+ const data = readRateData();
30816
+ const currentDate = today();
30817
+ const count = data.date === currentDate ? data.count : 0;
30818
+ const allowed = count < DAILY_LIMIT;
30819
+ const warning = count >= DAILY_LIMIT * WARNING_THRESHOLD;
30820
+ return { allowed, count, limit: DAILY_LIMIT, warning };
30821
+ }
30822
+ function getRateLimitStatus() {
30823
+ const data = readRateData();
30824
+ const currentDate = today();
30825
+ const count = data.date === currentDate ? data.count : 0;
30826
+ return {
30827
+ date: currentDate,
30828
+ count,
30829
+ limit: DAILY_LIMIT,
30830
+ remaining: DAILY_LIMIT - count
30831
+ };
30832
+ }
30833
+
30834
+ // src/client/retry.ts
30835
+ var DEFAULT_MAX_RETRIES = 3;
30836
+ var DEFAULT_BASE_DELAY = 1e3;
30837
+ function isNetworkError(error48) {
30838
+ if (error48 instanceof TypeError) {
30839
+ return true;
30840
+ }
30841
+ if (error48 instanceof Error) {
30842
+ const msg = error48.message.toLowerCase();
30843
+ return msg.includes("econnrefused") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("network");
30844
+ }
30845
+ return false;
30846
+ }
30847
+ function delay(ms) {
30848
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
30849
+ }
30850
+ async function withRetry(fn, options) {
30851
+ const maxRetries = options?.maxRetries ?? DEFAULT_MAX_RETRIES;
30852
+ const baseDelay = options?.baseDelay ?? DEFAULT_BASE_DELAY;
30853
+ let lastError;
30854
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
30855
+ try {
30856
+ return await fn();
30857
+ } catch (err) {
30858
+ lastError = err;
30859
+ if (!isNetworkError(err) || attempt === maxRetries) {
30860
+ throw err;
30861
+ }
30862
+ const waitMs = baseDelay * Math.pow(2, attempt);
30863
+ await delay(waitMs);
30864
+ }
30865
+ }
30866
+ throw lastError;
30867
+ }
30868
+
30745
30869
  // src/client/client.ts
30746
30870
  var BASE_URL = "https://data-dbg.krx.co.kr";
30747
30871
  async function krxFetch(options) {
30748
- const { checkRateLimit: checkRateLimit2, incrementCallCount: incrementCallCount2 } = await Promise.resolve().then(() => (init_rate_limit(), rate_limit_exports));
30749
- const rateStatus = checkRateLimit2();
30872
+ const useCache = options.cache !== false;
30873
+ if (useCache) {
30874
+ const cached2 = getCached(options.endpoint, options.params);
30875
+ if (cached2) {
30876
+ return { success: true, data: cached2 };
30877
+ }
30878
+ }
30879
+ const rateStatus = checkRateLimit();
30750
30880
  if (!rateStatus.allowed) {
30751
30881
  return {
30752
30882
  success: false,
@@ -30762,15 +30892,18 @@ async function krxFetch(options) {
30762
30892
  );
30763
30893
  }
30764
30894
  const url2 = `${BASE_URL}${options.endpoint}`;
30765
- const response = await fetch(url2, {
30766
- method: "POST",
30767
- headers: {
30768
- AUTH_KEY: options.apiKey,
30769
- "Content-Type": "application/json; charset=utf-8"
30770
- },
30771
- body: JSON.stringify(options.params)
30772
- });
30773
- incrementCallCount2();
30895
+ const response = await withRetry(
30896
+ () => fetch(url2, {
30897
+ method: "POST",
30898
+ headers: {
30899
+ AUTH_KEY: options.apiKey,
30900
+ "Content-Type": "application/json; charset=utf-8"
30901
+ },
30902
+ body: JSON.stringify(options.params)
30903
+ }),
30904
+ { maxRetries: options.retries ?? 3 }
30905
+ );
30906
+ incrementCallCount();
30774
30907
  if (!response.ok) {
30775
30908
  let errorMsg = `HTTP ${response.status}: ${response.statusText}`;
30776
30909
  let errorCode;
@@ -30798,21 +30931,95 @@ async function krxFetch(options) {
30798
30931
  error: "Unexpected response format: missing OutBlock_1"
30799
30932
  };
30800
30933
  }
30934
+ if (useCache) {
30935
+ setCached(options.endpoint, options.params, outBlock);
30936
+ }
30801
30937
  return {
30802
30938
  success: true,
30803
30939
  data: outBlock
30804
30940
  };
30805
30941
  }
30806
30942
 
30943
+ // src/client/range-fetch.ts
30944
+ var DEFAULT_CONCURRENCY = 5;
30945
+ async function fetchWithConcurrency(tasks, concurrency) {
30946
+ const slots = new Array(tasks.length);
30947
+ let index = 0;
30948
+ async function runNext() {
30949
+ while (index < tasks.length) {
30950
+ const currentIndex = index;
30951
+ index += 1;
30952
+ const task = tasks[currentIndex];
30953
+ if (task) {
30954
+ const result = await task();
30955
+ slots[currentIndex] = Promise.resolve(result);
30956
+ }
30957
+ }
30958
+ }
30959
+ const workers = Array.from(
30960
+ { length: Math.min(concurrency, tasks.length) },
30961
+ () => runNext()
30962
+ );
30963
+ await Promise.all(workers);
30964
+ return Promise.all(slots);
30965
+ }
30966
+ async function fetchDateRange(options) {
30967
+ const { endpoint, from, to, apiKey, cache, extraParams } = options;
30968
+ const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
30969
+ if (from > to) {
30970
+ return {
30971
+ success: false,
30972
+ data: [],
30973
+ error: `'from' date (${from}) must not be after 'to' date (${to})`,
30974
+ fetchedDays: 0,
30975
+ failedDays: 0
30976
+ };
30977
+ }
30978
+ const tradingDays = getTradingDays(from, to);
30979
+ if (tradingDays.length === 0) {
30980
+ return { success: true, data: [], fetchedDays: 0, failedDays: 0 };
30981
+ }
30982
+ const tasks = tradingDays.map((day) => async () => {
30983
+ const params = {
30984
+ basDd: day,
30985
+ ...extraParams
30986
+ };
30987
+ return krxFetch({
30988
+ endpoint,
30989
+ params,
30990
+ apiKey,
30991
+ cache
30992
+ });
30993
+ });
30994
+ const results = await fetchWithConcurrency(tasks, concurrency);
30995
+ const failedDays = results.filter((r) => !r.success).length;
30996
+ const mergedData = results.filter((r) => r.success).flatMap((r) => [...r.data]);
30997
+ if (mergedData.length === 0 && failedDays > 0) {
30998
+ return {
30999
+ success: false,
31000
+ data: [],
31001
+ error: `All ${failedDays} trading day(s) failed to fetch`,
31002
+ fetchedDays: 0,
31003
+ failedDays
31004
+ };
31005
+ }
31006
+ return {
31007
+ success: true,
31008
+ data: mergedData,
31009
+ fetchedDays: tradingDays.length - failedDays,
31010
+ failedDays
31011
+ };
31012
+ }
31013
+
30807
31014
  // src/client/auth.ts
30808
- import * as fs2 from "node:fs";
30809
- import * as path2 from "node:path";
30810
- import * as os2 from "node:os";
30811
- var CONFIG_DIR2 = path2.join(os2.homedir(), ".krx-cli");
30812
- var CONFIG_FILE = path2.join(CONFIG_DIR2, "config.json");
31015
+ import * as fs3 from "node:fs";
31016
+ import * as path3 from "node:path";
31017
+ import * as os3 from "node:os";
31018
+ var CONFIG_DIR2 = path3.join(os3.homedir(), ".krx-cli");
31019
+ var CONFIG_FILE = path3.join(CONFIG_DIR2, "config.json");
30813
31020
  function readConfig() {
30814
31021
  try {
30815
- const raw = fs2.readFileSync(CONFIG_FILE, "utf-8");
31022
+ const raw = fs3.readFileSync(CONFIG_FILE, "utf-8");
30816
31023
  return JSON.parse(raw);
30817
31024
  } catch {
30818
31025
  return {};
@@ -30821,6 +31028,9 @@ function readConfig() {
30821
31028
  function getApiKey() {
30822
31029
  return process.env["KRX_API_KEY"] ?? readConfig().apiKey;
30823
31030
  }
31031
+ function getCachedServiceStatus() {
31032
+ return readConfig().serviceStatus ?? {};
31033
+ }
30824
31034
 
30825
31035
  // src/validator/index.ts
30826
31036
  var DATE_PATTERN = /^\d{8}$/;
@@ -30846,9 +31056,116 @@ function validateDate(value) {
30846
31056
  return result.data;
30847
31057
  }
30848
31058
 
31059
+ // src/utils/krx-number.ts
31060
+ function parseKrxNumber(value) {
31061
+ const cleaned = value.replace(/,/g, "");
31062
+ const num = parseFloat(cleaned);
31063
+ return isNaN(num) ? 0 : num;
31064
+ }
31065
+ function isKrxNumericString(value) {
31066
+ return /^-?[\d,]+\.?\d*$/.test(value.trim());
31067
+ }
31068
+
31069
+ // src/utils/filter.ts
31070
+ var OPERATOR_PATTERN = /^(\S+)\s+(>=|<=|==|!=|>|<)\s+(.+)$/;
31071
+ function parseFilterExpression(expression) {
31072
+ const trimmed = expression.trim().replace(/\s+/g, " ");
31073
+ const match = OPERATOR_PATTERN.exec(trimmed);
31074
+ if (!match || !match[1] || !match[2] || !match[3]) {
31075
+ return null;
31076
+ }
31077
+ return {
31078
+ field: match[1],
31079
+ operator: match[2],
31080
+ value: match[3].trim()
31081
+ };
31082
+ }
31083
+ function compareValues(fieldValue, operator, filterValue) {
31084
+ const isNumeric = isKrxNumericString(fieldValue) && isKrxNumericString(filterValue);
31085
+ if (isNumeric) {
31086
+ const a = parseKrxNumber(fieldValue);
31087
+ const b = parseKrxNumber(filterValue);
31088
+ switch (operator) {
31089
+ case ">":
31090
+ return a > b;
31091
+ case "<":
31092
+ return a < b;
31093
+ case ">=":
31094
+ return a >= b;
31095
+ case "<=":
31096
+ return a <= b;
31097
+ case "==":
31098
+ return a === b;
31099
+ case "!=":
31100
+ return a !== b;
31101
+ }
31102
+ }
31103
+ switch (operator) {
31104
+ case "==":
31105
+ return fieldValue === filterValue;
31106
+ case "!=":
31107
+ return fieldValue !== filterValue;
31108
+ case ">":
31109
+ return fieldValue > filterValue;
31110
+ case "<":
31111
+ return fieldValue < filterValue;
31112
+ case ">=":
31113
+ return fieldValue >= filterValue;
31114
+ case "<=":
31115
+ return fieldValue <= filterValue;
31116
+ default: {
31117
+ const _exhaustive = operator;
31118
+ void _exhaustive;
31119
+ return false;
31120
+ }
31121
+ }
31122
+ }
31123
+ function filterData(data, expression) {
31124
+ const parsed = parseFilterExpression(expression);
31125
+ if (!parsed) {
31126
+ return data;
31127
+ }
31128
+ return data.filter((row) => {
31129
+ const fieldValue = row[parsed.field];
31130
+ if (fieldValue === void 0 || fieldValue === null) {
31131
+ return false;
31132
+ }
31133
+ return compareValues(String(fieldValue), parsed.operator, parsed.value);
31134
+ });
31135
+ }
31136
+
31137
+ // src/utils/data-pipeline.ts
31138
+ function sortData(data, field, direction = "desc") {
31139
+ const sorted = [...data].sort((a, b) => {
31140
+ const aVal = String(a[field] ?? "");
31141
+ const bVal = String(b[field] ?? "");
31142
+ if (isKrxNumericString(aVal) && isKrxNumericString(bVal)) {
31143
+ return parseKrxNumber(aVal) - parseKrxNumber(bVal);
31144
+ }
31145
+ return aVal.localeCompare(bVal, "ko");
31146
+ });
31147
+ return direction === "desc" ? sorted.reverse() : sorted;
31148
+ }
31149
+ function limitData(data, limit) {
31150
+ return data.slice(0, limit);
31151
+ }
31152
+ function applyPipeline(data, options) {
31153
+ let result = data;
31154
+ if (options.filter) {
31155
+ result = filterData(result, options.filter);
31156
+ }
31157
+ if (options.sort) {
31158
+ result = sortData(result, options.sort, options.direction ?? "desc");
31159
+ }
31160
+ if (options.limit && options.limit > 0) {
31161
+ result = limitData(result, options.limit);
31162
+ }
31163
+ return result;
31164
+ }
31165
+
30849
31166
  // src/mcp/tools/index.ts
30850
- function getEndpointShortName(path3) {
30851
- return path3.split("/").pop() ?? path3;
31167
+ function getEndpointShortName(path5) {
31168
+ return path5.split("/").pop() ?? path5;
30852
31169
  }
30853
31170
  function buildDescription(categoryId, endpoints) {
30854
31171
  const category = CATEGORIES.find((c) => c.id === categoryId);
@@ -30874,6 +31191,19 @@ function buildInputSchema(endpoints) {
30874
31191
  date: external_exports.string().optional().describe(
30875
31192
  "Trading date in YYYYMMDD format (default: recent trading day)"
30876
31193
  ),
31194
+ date_from: external_exports.string().optional().describe(
31195
+ "Start date for range query (YYYYMMDD). Use with date_to for multi-day data."
31196
+ ),
31197
+ date_to: external_exports.string().optional().describe(
31198
+ "End date for range query (YYYYMMDD). Use with date_from for multi-day data."
31199
+ ),
31200
+ isuCd: external_exports.string().optional().describe("Stock/item code (ISU_CD) to filter a specific item"),
31201
+ sort: external_exports.string().optional().describe("Sort results by this field name"),
31202
+ sort_direction: external_exports.enum(["asc", "desc"]).optional().describe("Sort direction (default: desc)"),
31203
+ limit: external_exports.number().optional().describe("Limit number of results returned"),
31204
+ filter: external_exports.string().optional().describe(
31205
+ 'Filter expression (e.g. "FLUC_RT > 5", "MKT_NM == KOSPI"). Operators: >, <, >=, <=, ==, !='
31206
+ ),
30877
31207
  fields: external_exports.array(external_exports.string()).optional().describe("Filter response to these field names only")
30878
31208
  };
30879
31209
  }
@@ -30883,22 +31213,15 @@ function filterFields(data, fields) {
30883
31213
  const filtered = {};
30884
31214
  for (const key of Object.keys(row)) {
30885
31215
  if (fieldSet.has(key)) {
30886
- filtered[key] = row[key];
31216
+ const value = row[key];
31217
+ if (value !== void 0) {
31218
+ filtered[key] = value;
31219
+ }
30887
31220
  }
30888
31221
  }
30889
31222
  return filtered;
30890
31223
  });
30891
31224
  }
30892
- function getRecentTradingDate() {
30893
- const now = /* @__PURE__ */ new Date();
30894
- const day = now.getDay();
30895
- const daysBack = day === 0 ? 2 : day === 6 ? 1 : day === 1 ? 3 : 1;
30896
- const target = new Date(now.getTime() - daysBack * 24 * 60 * 60 * 1e3);
30897
- const yyyy = target.getFullYear().toString();
30898
- const mm = (target.getMonth() + 1).toString().padStart(2, "0");
30899
- const dd = target.getDate().toString().padStart(2, "0");
30900
- return `${yyyy}${mm}${dd}`;
30901
- }
30902
31225
  function errorResult(message) {
30903
31226
  return {
30904
31227
  content: [
@@ -30927,6 +31250,59 @@ function createCategoryTool(categoryId) {
30927
31250
  if (!endpoint) {
30928
31251
  return errorResult(`Unknown endpoint: ${shortName}`);
30929
31252
  }
31253
+ const dateFrom = args.date_from;
31254
+ const dateTo = args.date_to;
31255
+ const isuCd = args.isuCd;
31256
+ if (dateFrom && !dateTo || !dateFrom && dateTo) {
31257
+ return errorResult(
31258
+ "Both date_from and date_to must be provided together"
31259
+ );
31260
+ }
31261
+ const isDateRange = dateFrom && dateTo;
31262
+ if (isDateRange) {
31263
+ try {
31264
+ validateDate(dateFrom);
31265
+ validateDate(dateTo);
31266
+ } catch (err) {
31267
+ return errorResult(
31268
+ err instanceof Error ? err.message : "Invalid date format"
31269
+ );
31270
+ }
31271
+ const extraParams = {};
31272
+ if (isuCd) {
31273
+ extraParams["isuCd"] = isuCd;
31274
+ }
31275
+ const rangeResult = await fetchDateRange({
31276
+ endpoint: endpoint.path,
31277
+ from: dateFrom,
31278
+ to: dateTo,
31279
+ apiKey,
31280
+ extraParams
31281
+ });
31282
+ if (!rangeResult.success) {
31283
+ return errorResult(rangeResult.error ?? "Date range fetch failed");
31284
+ }
31285
+ let data2 = rangeResult.data;
31286
+ const filterExpr = args.filter;
31287
+ const sortField2 = args.sort;
31288
+ const sortDirection2 = args.sort_direction ?? "desc";
31289
+ const limitN2 = args.limit;
31290
+ data2 = applyPipeline(data2, {
31291
+ filter: filterExpr,
31292
+ sort: sortField2,
31293
+ direction: sortDirection2,
31294
+ limit: limitN2
31295
+ });
31296
+ const fields2 = args.fields;
31297
+ if (fields2) {
31298
+ data2 = filterFields(data2, fields2);
31299
+ }
31300
+ return {
31301
+ content: [
31302
+ { type: "text", text: JSON.stringify(data2, null, 2) }
31303
+ ]
31304
+ };
31305
+ }
30930
31306
  const dateStr = args.date ?? getRecentTradingDate();
30931
31307
  try {
30932
31308
  validateDate(dateStr);
@@ -30935,16 +31311,33 @@ function createCategoryTool(categoryId) {
30935
31311
  err instanceof Error ? err.message : "Invalid date format"
30936
31312
  );
30937
31313
  }
31314
+ const params = { basDd: dateStr };
31315
+ if (isuCd) {
31316
+ params["isuCd"] = isuCd;
31317
+ }
30938
31318
  const result = await krxFetch({
30939
31319
  endpoint: endpoint.path,
30940
- params: { basDd: dateStr },
31320
+ params,
30941
31321
  apiKey
30942
31322
  });
30943
31323
  if (!result.success) {
30944
31324
  return errorResult(result.error ?? "API request failed");
30945
31325
  }
31326
+ const filterExpr2 = args.filter;
31327
+ const sortField = args.sort;
31328
+ const sortDirection = args.sort_direction ?? "desc";
31329
+ const limitN = args.limit;
31330
+ let data = result.data;
31331
+ data = applyPipeline(data, {
31332
+ filter: filterExpr2,
31333
+ sort: sortField,
31334
+ direction: sortDirection,
31335
+ limit: limitN
31336
+ });
30946
31337
  const fields = args.fields;
30947
- const data = fields ? filterFields(result.data, fields) : result.data;
31338
+ if (fields) {
31339
+ data = filterFields(data, fields);
31340
+ }
30948
31341
  return {
30949
31342
  content: [
30950
31343
  { type: "text", text: JSON.stringify(data, null, 2) }
@@ -30958,8 +31351,8 @@ function createCategoryTools() {
30958
31351
  }
30959
31352
 
30960
31353
  // src/mcp/tools/schema-tool.ts
30961
- function getEndpointShortName2(path3) {
30962
- return path3.split("/").pop() ?? path3;
31354
+ function getEndpointShortName2(path5) {
31355
+ return path5.split("/").pop() ?? path5;
30963
31356
  }
30964
31357
  function createSchemaTool() {
30965
31358
  const allShortNames = ENDPOINTS.map((e) => getEndpointShortName2(e.path));
@@ -31020,7 +31413,6 @@ function createSchemaTool() {
31020
31413
  }
31021
31414
 
31022
31415
  // src/mcp/tools/rate-limit-tool.ts
31023
- init_rate_limit();
31024
31416
  function createRateLimitTool() {
31025
31417
  return {
31026
31418
  name: "krx_rate_limit",
@@ -31037,22 +31429,591 @@ function createRateLimitTool() {
31037
31429
  };
31038
31430
  }
31039
31431
 
31432
+ // src/client/search.ts
31433
+ var BASE_INFO_ENDPOINTS = [
31434
+ { endpoint: "/svc/apis/sto/stk_isu_base_info", market: "KOSPI" },
31435
+ { endpoint: "/svc/apis/sto/ksq_isu_base_info", market: "KOSDAQ" }
31436
+ ];
31437
+ async function searchStock(apiKey, query) {
31438
+ const basDd = getRecentTradingDate();
31439
+ const lowerQuery = query.toLowerCase();
31440
+ const results = await Promise.all(
31441
+ BASE_INFO_ENDPOINTS.map(async ({ endpoint, market }) => {
31442
+ const result = await krxFetch({
31443
+ endpoint,
31444
+ params: { basDd },
31445
+ apiKey
31446
+ });
31447
+ if (!result.success) {
31448
+ return [];
31449
+ }
31450
+ return result.data.filter((row) => {
31451
+ const name = row["ISU_NM"] ?? "";
31452
+ const shortName = row["ISU_ABBRV"] ?? "";
31453
+ return name.toLowerCase().includes(lowerQuery) || shortName.toLowerCase().includes(lowerQuery);
31454
+ }).map((row) => ({
31455
+ ISU_CD: row["ISU_CD"] ?? "",
31456
+ ISU_SRT_CD: row["ISU_SRT_CD"] ?? "",
31457
+ ISU_NM: row["ISU_NM"] ?? row["ISU_ABBRV"] ?? "",
31458
+ MKT_NM: market
31459
+ }));
31460
+ })
31461
+ );
31462
+ return results.flat();
31463
+ }
31464
+
31465
+ // src/mcp/tools/search-tool.ts
31466
+ function createSearchTool() {
31467
+ return {
31468
+ name: "krx_search",
31469
+ description: "Search KRX stocks by name. Returns matching stock codes (ISU_CD), short codes (ISU_SRT_CD), names (ISU_NM), and market (KOSPI/KOSDAQ). Use this to resolve a stock name to its code before querying market data.",
31470
+ inputSchema: {
31471
+ query: external_exports.string().describe(
31472
+ "Stock name or partial name to search (e.g., '\uC0BC\uC131\uC804\uC790', '\uCE74\uCE74\uC624')"
31473
+ )
31474
+ },
31475
+ handler: async (args) => {
31476
+ const query = args.query;
31477
+ const apiKey = getApiKey();
31478
+ if (!apiKey) {
31479
+ return {
31480
+ content: [
31481
+ {
31482
+ type: "text",
31483
+ text: JSON.stringify({
31484
+ error: "API key not configured. Set KRX_API_KEY environment variable."
31485
+ })
31486
+ }
31487
+ ],
31488
+ isError: true
31489
+ };
31490
+ }
31491
+ const results = await searchStock(apiKey, query);
31492
+ if (results.length === 0) {
31493
+ return {
31494
+ content: [
31495
+ {
31496
+ type: "text",
31497
+ text: JSON.stringify({
31498
+ error: `No stocks found matching "${query}"`
31499
+ })
31500
+ }
31501
+ ],
31502
+ isError: true
31503
+ };
31504
+ }
31505
+ return {
31506
+ content: [
31507
+ { type: "text", text: JSON.stringify(results, null, 2) }
31508
+ ]
31509
+ };
31510
+ }
31511
+ };
31512
+ }
31513
+
31514
+ // src/client/market-summary.ts
31515
+ var KOSPI_INDEX_ENDPOINT = "/svc/apis/idx/kospi_dd_trd";
31516
+ var KOSDAQ_INDEX_ENDPOINT = "/svc/apis/idx/kosdaq_dd_trd";
31517
+ var KOSPI_STOCK_ENDPOINT = "/svc/apis/sto/stk_bydd_trd";
31518
+ var KOSDAQ_STOCK_ENDPOINT = "/svc/apis/sto/ksq_bydd_trd";
31519
+ var TOP_N = 5;
31520
+ function computeStockStats(stocks) {
31521
+ return stocks.reduce(
31522
+ (acc, stock) => {
31523
+ const rate = parseKrxNumber(stock["FLUC_RT"] ?? "0");
31524
+ return {
31525
+ advancing: acc.advancing + (rate > 0 ? 1 : 0),
31526
+ declining: acc.declining + (rate < 0 ? 1 : 0),
31527
+ unchanged: acc.unchanged + (rate === 0 ? 1 : 0),
31528
+ totalVolume: acc.totalVolume + parseKrxNumber(stock["ACC_TRDVOL"] ?? "0"),
31529
+ totalValue: acc.totalValue + parseKrxNumber(stock["ACC_TRDVAL"] ?? "0")
31530
+ };
31531
+ },
31532
+ {
31533
+ advancing: 0,
31534
+ declining: 0,
31535
+ unchanged: 0,
31536
+ totalVolume: 0,
31537
+ totalValue: 0
31538
+ }
31539
+ );
31540
+ }
31541
+ function computeTopMovers(stocks) {
31542
+ const sorted = [...stocks].sort((a, b) => {
31543
+ const rateA = parseKrxNumber(a["FLUC_RT"] ?? "0");
31544
+ const rateB = parseKrxNumber(b["FLUC_RT"] ?? "0");
31545
+ return rateB - rateA;
31546
+ });
31547
+ const topGainers = sorted.slice(0, TOP_N);
31548
+ const topLosers = sorted.slice(-TOP_N).reverse();
31549
+ return { topGainers, topLosers };
31550
+ }
31551
+ function safeFetch(...args) {
31552
+ return krxFetch(...args).catch(
31553
+ (err) => ({
31554
+ success: false,
31555
+ data: [],
31556
+ error: err instanceof Error ? err.message : "Network error"
31557
+ })
31558
+ );
31559
+ }
31560
+ async function fetchMarketSummary(options) {
31561
+ const { apiKey, date: date5, cache } = options;
31562
+ const params = { basDd: date5 };
31563
+ const [kospiIdx, kosdaqIdx, kospiStk, kosdaqStk] = await Promise.all([
31564
+ safeFetch({ endpoint: KOSPI_INDEX_ENDPOINT, params, apiKey, cache }),
31565
+ safeFetch({ endpoint: KOSDAQ_INDEX_ENDPOINT, params, apiKey, cache }),
31566
+ safeFetch({ endpoint: KOSPI_STOCK_ENDPOINT, params, apiKey, cache }),
31567
+ safeFetch({ endpoint: KOSDAQ_STOCK_ENDPOINT, params, apiKey, cache })
31568
+ ]);
31569
+ const allFailed = !kospiIdx.success && !kosdaqIdx.success && !kospiStk.success && !kosdaqStk.success;
31570
+ if (allFailed) {
31571
+ return {
31572
+ success: false,
31573
+ error: "All market data fetches failed"
31574
+ };
31575
+ }
31576
+ const kospiIndexData = kospiIdx.success ? kospiIdx.data : [];
31577
+ const kosdaqIndexData = kosdaqIdx.success ? kosdaqIdx.data : [];
31578
+ const kospiStockData = kospiStk.success ? kospiStk.data : [];
31579
+ const kosdaqStockData = kosdaqStk.success ? kosdaqStk.data : [];
31580
+ const allStocks = [...kospiStockData, ...kosdaqStockData];
31581
+ const stockStats = computeStockStats(allStocks);
31582
+ const { topGainers, topLosers } = computeTopMovers(allStocks);
31583
+ return {
31584
+ success: true,
31585
+ data: {
31586
+ date: date5,
31587
+ kospiIndex: kospiIndexData,
31588
+ kosdaqIndex: kosdaqIndexData,
31589
+ stockStats,
31590
+ topGainers,
31591
+ topLosers
31592
+ }
31593
+ };
31594
+ }
31595
+
31596
+ // src/mcp/tools/market-summary-tool.ts
31597
+ function createMarketSummaryTool() {
31598
+ return {
31599
+ name: "krx_market_summary",
31600
+ description: `Get a comprehensive market summary including KOSPI/KOSDAQ indices, stock statistics (advancing/declining/unchanged), top 5 gainers/losers, and total trading volume/value.
31601
+
31602
+ This tool combines data from 4 API endpoints in a single call \u2014 ideal for getting a quick market overview.
31603
+
31604
+ Returns:
31605
+ - kospiIndex: KOSPI series index data
31606
+ - kosdaqIndex: KOSDAQ series index data
31607
+ - stockStats: { advancing, declining, unchanged, totalVolume, totalValue }
31608
+ - topGainers: Top 5 stocks by change rate
31609
+ - topLosers: Bottom 5 stocks by change rate`,
31610
+ inputSchema: {
31611
+ date: external_exports.string().optional().describe(
31612
+ "Trading date in YYYYMMDD format (default: recent trading day)"
31613
+ )
31614
+ },
31615
+ handler: async (args) => {
31616
+ const apiKey = getApiKey();
31617
+ if (!apiKey) {
31618
+ return {
31619
+ content: [
31620
+ {
31621
+ type: "text",
31622
+ text: JSON.stringify({
31623
+ error: "API key not configured. Set KRX_API_KEY environment variable."
31624
+ })
31625
+ }
31626
+ ],
31627
+ isError: true
31628
+ };
31629
+ }
31630
+ const dateStr = args.date ?? getRecentTradingDate();
31631
+ try {
31632
+ validateDate(dateStr);
31633
+ } catch (err) {
31634
+ return {
31635
+ content: [
31636
+ {
31637
+ type: "text",
31638
+ text: JSON.stringify({
31639
+ error: err instanceof Error ? err.message : "Invalid date"
31640
+ })
31641
+ }
31642
+ ],
31643
+ isError: true
31644
+ };
31645
+ }
31646
+ const result = await fetchMarketSummary({
31647
+ apiKey,
31648
+ date: dateStr
31649
+ });
31650
+ if (!result.success) {
31651
+ return {
31652
+ content: [
31653
+ {
31654
+ type: "text",
31655
+ text: JSON.stringify({
31656
+ error: result.error ?? "Failed to fetch market summary"
31657
+ })
31658
+ }
31659
+ ],
31660
+ isError: true
31661
+ };
31662
+ }
31663
+ return {
31664
+ content: [
31665
+ {
31666
+ type: "text",
31667
+ text: JSON.stringify(result.data, null, 2)
31668
+ }
31669
+ ]
31670
+ };
31671
+ }
31672
+ };
31673
+ }
31674
+
31675
+ // src/watchlist/store.ts
31676
+ import * as fs4 from "node:fs";
31677
+ import * as path4 from "node:path";
31678
+ import * as os4 from "node:os";
31679
+ function getWatchlistPath() {
31680
+ return path4.join(os4.homedir(), ".krx-cli", "watchlist.json");
31681
+ }
31682
+ function getWatchlist() {
31683
+ const filePath = getWatchlistPath();
31684
+ if (!fs4.existsSync(filePath)) {
31685
+ return [];
31686
+ }
31687
+ try {
31688
+ const raw = fs4.readFileSync(filePath, "utf-8");
31689
+ return JSON.parse(raw);
31690
+ } catch (err) {
31691
+ process.stderr.write(`[krx-cli] watchlist read failed: ${String(err)}
31692
+ `);
31693
+ return [];
31694
+ }
31695
+ }
31696
+ function saveWatchlist(entries) {
31697
+ const filePath = getWatchlistPath();
31698
+ const dir = path4.dirname(filePath);
31699
+ try {
31700
+ fs4.mkdirSync(dir, { recursive: true });
31701
+ fs4.writeFileSync(filePath, JSON.stringify(entries, null, 2), "utf-8");
31702
+ return true;
31703
+ } catch (err) {
31704
+ process.stderr.write(`[krx-cli] watchlist write failed: ${String(err)}
31705
+ `);
31706
+ return false;
31707
+ }
31708
+ }
31709
+ function addToWatchlist(entry) {
31710
+ const current = getWatchlist();
31711
+ const isDuplicate = current.some((e) => e.isuCd === entry.isuCd);
31712
+ if (isDuplicate) {
31713
+ return { added: false, reason: "duplicate" };
31714
+ }
31715
+ const saved = saveWatchlist([...current, entry]);
31716
+ return saved ? { added: true } : { added: false, reason: "write_error" };
31717
+ }
31718
+ function removeFromWatchlist(nameOrCode) {
31719
+ const current = getWatchlist();
31720
+ const filtered = current.filter(
31721
+ (e) => e.isuCd !== nameOrCode && e.name !== nameOrCode
31722
+ );
31723
+ if (filtered.length === current.length) {
31724
+ return { removed: false, reason: "not_found" };
31725
+ }
31726
+ const saved = saveWatchlist(filtered);
31727
+ return saved ? { removed: true } : { removed: false, reason: "write_error" };
31728
+ }
31729
+
31730
+ // src/mcp/tools/watchlist-tool.ts
31731
+ var STOCK_ENDPOINTS = {
31732
+ KOSPI: "/svc/apis/sto/stk_bydd_trd",
31733
+ KOSDAQ: "/svc/apis/sto/ksq_bydd_trd"
31734
+ };
31735
+ function textResult(data, isError = false) {
31736
+ return {
31737
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
31738
+ ...isError ? { isError: true } : {}
31739
+ };
31740
+ }
31741
+ async function handleAdd(name) {
31742
+ const apiKey = getApiKey();
31743
+ if (!apiKey) {
31744
+ return textResult({ error: "API key not configured" }, true);
31745
+ }
31746
+ let results;
31747
+ try {
31748
+ results = await searchStock(apiKey, name);
31749
+ } catch (err) {
31750
+ return textResult(
31751
+ {
31752
+ error: `Search failed: ${err instanceof Error ? err.message : String(err)}`
31753
+ },
31754
+ true
31755
+ );
31756
+ }
31757
+ if (results.length === 0) {
31758
+ return textResult({ error: `No stock found matching '${name}'` }, true);
31759
+ }
31760
+ if (results.length > 1) {
31761
+ return textResult({
31762
+ message: `Multiple matches for '${name}'. Be more specific.`,
31763
+ matches: results
31764
+ });
31765
+ }
31766
+ const stock = results[0];
31767
+ const result = addToWatchlist({
31768
+ isuCd: stock.ISU_CD,
31769
+ isuSrtCd: stock.ISU_SRT_CD,
31770
+ name: stock.ISU_NM,
31771
+ market: stock.MKT_NM
31772
+ });
31773
+ if (!result.added) {
31774
+ const message = result.reason === "write_error" ? "Failed to save watchlist" : `'${stock.ISU_NM}' is already in watchlist`;
31775
+ return textResult({ error: message }, result.reason === "write_error");
31776
+ }
31777
+ return textResult({
31778
+ message: `Added '${stock.ISU_NM}' (${stock.ISU_CD})`,
31779
+ entry: { isuCd: stock.ISU_CD, name: stock.ISU_NM, market: stock.MKT_NM }
31780
+ });
31781
+ }
31782
+ async function handleShow(dateArg) {
31783
+ const apiKey = getApiKey();
31784
+ if (!apiKey) {
31785
+ return textResult({ error: "API key not configured" }, true);
31786
+ }
31787
+ const entries = getWatchlist();
31788
+ if (entries.length === 0) {
31789
+ return textResult({ message: "Watchlist is empty", entries: [] });
31790
+ }
31791
+ const date5 = dateArg ?? getRecentTradingDate();
31792
+ try {
31793
+ validateDate(date5);
31794
+ } catch (err) {
31795
+ return textResult(
31796
+ { error: err instanceof Error ? err.message : "Invalid date" },
31797
+ true
31798
+ );
31799
+ }
31800
+ const isuCds = new Set(entries.map((e) => e.isuCd));
31801
+ const errors = [];
31802
+ const [kospiResult, kosdaqResult] = await Promise.all([
31803
+ krxFetch({
31804
+ endpoint: STOCK_ENDPOINTS.KOSPI,
31805
+ params: { basDd: date5 },
31806
+ apiKey
31807
+ }).catch((err) => {
31808
+ errors.push(`KOSPI: ${err instanceof Error ? err.message : String(err)}`);
31809
+ return { success: false, data: [] };
31810
+ }),
31811
+ krxFetch({
31812
+ endpoint: STOCK_ENDPOINTS.KOSDAQ,
31813
+ params: { basDd: date5 },
31814
+ apiKey
31815
+ }).catch((err) => {
31816
+ errors.push(
31817
+ `KOSDAQ: ${err instanceof Error ? err.message : String(err)}`
31818
+ );
31819
+ return { success: false, data: [] };
31820
+ })
31821
+ ]);
31822
+ if (!kospiResult.success && !kosdaqResult.success) {
31823
+ return textResult(
31824
+ { error: `All market data fetches failed: ${errors.join("; ")}` },
31825
+ true
31826
+ );
31827
+ }
31828
+ const allStocks = [
31829
+ ...kospiResult.success ? kospiResult.data : [],
31830
+ ...kosdaqResult.success ? kosdaqResult.data : []
31831
+ ];
31832
+ const watchlistData = allStocks.filter((s) => isuCds.has(s["ISU_CD"] ?? ""));
31833
+ const output = { date: date5, stocks: watchlistData };
31834
+ if (errors.length > 0) {
31835
+ output["warnings"] = errors;
31836
+ }
31837
+ return textResult(output);
31838
+ }
31839
+ function createWatchlistTool() {
31840
+ return {
31841
+ name: "krx_watchlist",
31842
+ description: `Manage a persistent watchlist of stocks.
31843
+
31844
+ Actions:
31845
+ - add: Search by name and add to watchlist
31846
+ - remove: Remove by name or stock code (exact match)
31847
+ - list: Show all watchlist entries
31848
+ - show: Fetch current prices for all watchlist stocks
31849
+
31850
+ The watchlist is stored locally at ~/.krx-cli/watchlist.json.`,
31851
+ inputSchema: {
31852
+ action: external_exports.enum(["add", "remove", "list", "show"]).describe("Action to perform"),
31853
+ name: external_exports.string().optional().describe("Stock name or code (required for add/remove)"),
31854
+ date: external_exports.string().optional().describe("Trading date YYYYMMDD (for show action)")
31855
+ },
31856
+ handler: async (args) => {
31857
+ const action = args.action;
31858
+ const name = args.name;
31859
+ const date5 = args.date;
31860
+ switch (action) {
31861
+ case "add": {
31862
+ if (!name) {
31863
+ return textResult(
31864
+ { error: "name is required for add action" },
31865
+ true
31866
+ );
31867
+ }
31868
+ return handleAdd(name);
31869
+ }
31870
+ case "remove": {
31871
+ if (!name) {
31872
+ return textResult(
31873
+ { error: "name is required for remove action" },
31874
+ true
31875
+ );
31876
+ }
31877
+ const result = removeFromWatchlist(name);
31878
+ if (!result.removed) {
31879
+ const message = result.reason === "write_error" ? "Failed to save watchlist" : `'${name}' not found in watchlist`;
31880
+ return textResult({ error: message }, true);
31881
+ }
31882
+ return textResult({ message: `Removed '${name}' from watchlist` });
31883
+ }
31884
+ case "list": {
31885
+ const entries = getWatchlist();
31886
+ return textResult(
31887
+ entries.length === 0 ? { message: "Watchlist is empty", entries: [] } : entries
31888
+ );
31889
+ }
31890
+ case "show": {
31891
+ return handleShow(date5);
31892
+ }
31893
+ default: {
31894
+ return textResult({ error: `Unknown action: ${action}` }, true);
31895
+ }
31896
+ }
31897
+ }
31898
+ };
31899
+ }
31900
+
31901
+ // src/mcp/resources/index.ts
31902
+ function jsonContent(uri, data) {
31903
+ return {
31904
+ contents: [
31905
+ {
31906
+ uri: uri.href,
31907
+ mimeType: "application/json",
31908
+ text: JSON.stringify(data)
31909
+ }
31910
+ ]
31911
+ };
31912
+ }
31913
+ function errorContent(uri, message) {
31914
+ return jsonContent(uri, { error: message });
31915
+ }
31916
+ function createWatchlistResource() {
31917
+ return {
31918
+ name: "watchlist",
31919
+ uri: "krx://watchlist",
31920
+ handler: async (uri) => {
31921
+ try {
31922
+ const entries = getWatchlist();
31923
+ return jsonContent(uri, entries);
31924
+ } catch {
31925
+ return errorContent(uri, "Failed to read watchlist");
31926
+ }
31927
+ }
31928
+ };
31929
+ }
31930
+ function createRateLimitResource() {
31931
+ return {
31932
+ name: "rate-limit",
31933
+ uri: "krx://rate-limit",
31934
+ handler: async (uri) => {
31935
+ try {
31936
+ const status = getRateLimitStatus();
31937
+ return jsonContent(uri, status);
31938
+ } catch {
31939
+ return errorContent(uri, "Failed to read rate limit status");
31940
+ }
31941
+ }
31942
+ };
31943
+ }
31944
+ function createServiceStatusResource() {
31945
+ return {
31946
+ name: "service-status",
31947
+ uri: "krx://service-status",
31948
+ handler: async (uri) => {
31949
+ try {
31950
+ const rawStatus = getCachedServiceStatus();
31951
+ const safeStatus = Object.fromEntries(
31952
+ Object.entries(rawStatus).filter(([key]) => key !== "apiKey")
31953
+ );
31954
+ return jsonContent(uri, safeStatus);
31955
+ } catch {
31956
+ return errorContent(uri, "Failed to read service status");
31957
+ }
31958
+ }
31959
+ };
31960
+ }
31961
+
31962
+ // src/cli/commands/version.ts
31963
+ import { readFileSync as readFileSync5 } from "node:fs";
31964
+ import { fileURLToPath } from "node:url";
31965
+ import { dirname as dirname3, resolve } from "node:path";
31966
+ function getLocalVersion() {
31967
+ const __filename = fileURLToPath(import.meta.url);
31968
+ const __dirname = dirname3(__filename);
31969
+ const candidates = [
31970
+ resolve(__dirname, "..", "package.json"),
31971
+ resolve(__dirname, "..", "..", "..", "package.json")
31972
+ ];
31973
+ for (const candidate of candidates) {
31974
+ try {
31975
+ const raw = readFileSync5(candidate, "utf-8");
31976
+ const pkg = JSON.parse(raw);
31977
+ return pkg.version;
31978
+ } catch {
31979
+ continue;
31980
+ }
31981
+ }
31982
+ return "unknown";
31983
+ }
31984
+ function getVersion() {
31985
+ return getLocalVersion();
31986
+ }
31987
+
31040
31988
  // src/mcp/server.ts
31041
31989
  function createServer() {
31042
31990
  const server2 = new McpServer({
31043
31991
  name: "krx-cli",
31044
- version: "1.2.0"
31992
+ version: getVersion()
31045
31993
  });
31046
31994
  const allTools = [
31047
31995
  ...createCategoryTools(),
31048
31996
  createSchemaTool(),
31049
- createRateLimitTool()
31997
+ createRateLimitTool(),
31998
+ createSearchTool(),
31999
+ createMarketSummaryTool(),
32000
+ createWatchlistTool()
31050
32001
  ];
31051
32002
  for (const tool of allTools) {
31052
32003
  server2.tool(tool.name, tool.description, tool.inputSchema, async (args) => {
31053
32004
  return tool.handler(args);
31054
32005
  });
31055
32006
  }
32007
+ const allResources = [
32008
+ createWatchlistResource(),
32009
+ createRateLimitResource(),
32010
+ createServiceStatusResource()
32011
+ ];
32012
+ for (const resource of allResources) {
32013
+ server2.resource(resource.name, resource.uri, async (uri) => {
32014
+ return resource.handler(uri);
32015
+ });
32016
+ }
31056
32017
  return server2;
31057
32018
  }
31058
32019