krx-cli 1.3.1 → 1.6.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/README.md +119 -34
- package/SKILL.md +99 -4
- package/dist/cli.js +55096 -12356
- package/dist/cli.js.map +4 -4
- package/dist/mcp.js +1102 -144
- package/dist/mcp.js.map +4 -4
- package/package.json +2 -1
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 =
|
|
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
|
|
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(
|
|
3230
|
-
let input =
|
|
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 [
|
|
3430
|
-
wsComponent.path =
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
7642
|
-
if (!
|
|
7572
|
+
function getElementAtPath(obj, path5) {
|
|
7573
|
+
if (!path5)
|
|
7643
7574
|
return obj;
|
|
7644
|
-
return
|
|
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(
|
|
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(
|
|
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,
|
|
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 = [...
|
|
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
|
|
8257
|
-
for (const seg of
|
|
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
|
|
20235
|
-
if (
|
|
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 (
|
|
20240
|
-
const key =
|
|
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((
|
|
22169
|
+
return new Promise((resolve2) => {
|
|
22239
22170
|
const json2 = serializeMessage(message);
|
|
22240
22171
|
if (this._stdout.write(json2)) {
|
|
22241
|
-
|
|
22172
|
+
resolve2();
|
|
22242
22173
|
} else {
|
|
22243
|
-
this._stdout.once("drain",
|
|
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:
|
|
22609
|
-
const fullPath = [...
|
|
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,
|
|
22655
|
+
constructor(parent, value, path5, key) {
|
|
22725
22656
|
this._cachedPath = [];
|
|
22726
22657
|
this.parent = parent;
|
|
22727
22658
|
this.data = value;
|
|
22728
|
-
this._path =
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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(
|
|
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((
|
|
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(
|
|
30481
|
+
function ep(path5, description, descriptionKo, category) {
|
|
30551
30482
|
return {
|
|
30552
|
-
path:
|
|
30483
|
+
path: path5,
|
|
30553
30484
|
description,
|
|
30554
30485
|
descriptionKo,
|
|
30555
30486
|
category,
|
|
30556
|
-
responseFields: RESPONSE_FIELDS[
|
|
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
|
|
30749
|
-
|
|
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
|
|
30766
|
-
|
|
30767
|
-
|
|
30768
|
-
|
|
30769
|
-
|
|
30770
|
-
|
|
30771
|
-
|
|
30772
|
-
|
|
30773
|
-
|
|
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
|
|
30809
|
-
import * as
|
|
30810
|
-
import * as
|
|
30811
|
-
var CONFIG_DIR2 =
|
|
30812
|
-
var CONFIG_FILE =
|
|
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 =
|
|
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(
|
|
30851
|
-
return
|
|
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
|
}
|
|
@@ -30892,16 +31222,6 @@ function filterFields(data, fields) {
|
|
|
30892
31222
|
return filtered;
|
|
30893
31223
|
});
|
|
30894
31224
|
}
|
|
30895
|
-
function getRecentTradingDate() {
|
|
30896
|
-
const now = /* @__PURE__ */ new Date();
|
|
30897
|
-
const day = now.getDay();
|
|
30898
|
-
const daysBack = day === 0 ? 2 : day === 6 ? 1 : day === 1 ? 3 : 1;
|
|
30899
|
-
const target = new Date(now.getTime() - daysBack * 24 * 60 * 60 * 1e3);
|
|
30900
|
-
const yyyy = target.getFullYear().toString();
|
|
30901
|
-
const mm = (target.getMonth() + 1).toString().padStart(2, "0");
|
|
30902
|
-
const dd = target.getDate().toString().padStart(2, "0");
|
|
30903
|
-
return `${yyyy}${mm}${dd}`;
|
|
30904
|
-
}
|
|
30905
31225
|
function errorResult(message) {
|
|
30906
31226
|
return {
|
|
30907
31227
|
content: [
|
|
@@ -30930,6 +31250,59 @@ function createCategoryTool(categoryId) {
|
|
|
30930
31250
|
if (!endpoint) {
|
|
30931
31251
|
return errorResult(`Unknown endpoint: ${shortName}`);
|
|
30932
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
|
+
}
|
|
30933
31306
|
const dateStr = args.date ?? getRecentTradingDate();
|
|
30934
31307
|
try {
|
|
30935
31308
|
validateDate(dateStr);
|
|
@@ -30938,16 +31311,33 @@ function createCategoryTool(categoryId) {
|
|
|
30938
31311
|
err instanceof Error ? err.message : "Invalid date format"
|
|
30939
31312
|
);
|
|
30940
31313
|
}
|
|
31314
|
+
const params = { basDd: dateStr };
|
|
31315
|
+
if (isuCd) {
|
|
31316
|
+
params["isuCd"] = isuCd;
|
|
31317
|
+
}
|
|
30941
31318
|
const result = await krxFetch({
|
|
30942
31319
|
endpoint: endpoint.path,
|
|
30943
|
-
params
|
|
31320
|
+
params,
|
|
30944
31321
|
apiKey
|
|
30945
31322
|
});
|
|
30946
31323
|
if (!result.success) {
|
|
30947
31324
|
return errorResult(result.error ?? "API request failed");
|
|
30948
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
|
+
});
|
|
30949
31337
|
const fields = args.fields;
|
|
30950
|
-
|
|
31338
|
+
if (fields) {
|
|
31339
|
+
data = filterFields(data, fields);
|
|
31340
|
+
}
|
|
30951
31341
|
return {
|
|
30952
31342
|
content: [
|
|
30953
31343
|
{ type: "text", text: JSON.stringify(data, null, 2) }
|
|
@@ -30961,8 +31351,8 @@ function createCategoryTools() {
|
|
|
30961
31351
|
}
|
|
30962
31352
|
|
|
30963
31353
|
// src/mcp/tools/schema-tool.ts
|
|
30964
|
-
function getEndpointShortName2(
|
|
30965
|
-
return
|
|
31354
|
+
function getEndpointShortName2(path5) {
|
|
31355
|
+
return path5.split("/").pop() ?? path5;
|
|
30966
31356
|
}
|
|
30967
31357
|
function createSchemaTool() {
|
|
30968
31358
|
const allShortNames = ENDPOINTS.map((e) => getEndpointShortName2(e.path));
|
|
@@ -31023,7 +31413,6 @@ function createSchemaTool() {
|
|
|
31023
31413
|
}
|
|
31024
31414
|
|
|
31025
31415
|
// src/mcp/tools/rate-limit-tool.ts
|
|
31026
|
-
init_rate_limit();
|
|
31027
31416
|
function createRateLimitTool() {
|
|
31028
31417
|
return {
|
|
31029
31418
|
name: "krx_rate_limit",
|
|
@@ -31040,22 +31429,591 @@ function createRateLimitTool() {
|
|
|
31040
31429
|
};
|
|
31041
31430
|
}
|
|
31042
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
|
+
|
|
31043
31988
|
// src/mcp/server.ts
|
|
31044
31989
|
function createServer() {
|
|
31045
31990
|
const server2 = new McpServer({
|
|
31046
31991
|
name: "krx-cli",
|
|
31047
|
-
version:
|
|
31992
|
+
version: getVersion()
|
|
31048
31993
|
});
|
|
31049
31994
|
const allTools = [
|
|
31050
31995
|
...createCategoryTools(),
|
|
31051
31996
|
createSchemaTool(),
|
|
31052
|
-
createRateLimitTool()
|
|
31997
|
+
createRateLimitTool(),
|
|
31998
|
+
createSearchTool(),
|
|
31999
|
+
createMarketSummaryTool(),
|
|
32000
|
+
createWatchlistTool()
|
|
31053
32001
|
];
|
|
31054
32002
|
for (const tool of allTools) {
|
|
31055
32003
|
server2.tool(tool.name, tool.description, tool.inputSchema, async (args) => {
|
|
31056
32004
|
return tool.handler(args);
|
|
31057
32005
|
});
|
|
31058
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
|
+
}
|
|
31059
32017
|
return server2;
|
|
31060
32018
|
}
|
|
31061
32019
|
|