@2oolkit/kiwoom-cli 0.1.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 ADDED
@@ -0,0 +1,1097 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/mcp.ts
27
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
28
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
29
+
30
+ // src/mcp/tools/market.ts
31
+ var import_zod = require("zod");
32
+
33
+ // src/config/constants.ts
34
+ var BASE_URLS = {
35
+ real: "https://api.kiwoom.com",
36
+ mock: "https://mockapi.kiwoom.com"
37
+ };
38
+ var ENV_ALIASES = {
39
+ real: "real",
40
+ prod: "real",
41
+ production: "real",
42
+ live: "real",
43
+ \uC2E4\uC804: "real",
44
+ \uC2E4\uC804\uD22C\uC790: "real",
45
+ mock: "mock",
46
+ test: "mock",
47
+ paper: "mock",
48
+ demo: "mock",
49
+ \uBAA8\uC758: "mock",
50
+ \uBAA8\uC758\uD22C\uC790: "mock"
51
+ };
52
+ var TOKEN_PATH = "/oauth2/token";
53
+ var REVOKE_PATH = "/oauth2/revoke";
54
+ var CONFIG_DIR_NAME = ".kiwoom-cli";
55
+ var CONFIG_FILE_NAME = "config.json";
56
+ var TOKEN_FILE_NAME = "token.json";
57
+ var ENV_VARS = {
58
+ appkey: "KIWOOM_APPKEY",
59
+ secretkey: "KIWOOM_SECRETKEY",
60
+ env: "KIWOOM_ENV"
61
+ };
62
+ var TOKEN_REFRESH_BUFFER_MS = 6e4;
63
+ var REQUEST_TIMEOUT_MS = 3e4;
64
+ var SOFT_EMPTY_RETURN_CODES = /* @__PURE__ */ new Set([20]);
65
+ var ORDER_EXCHANGE_TYPES = ["KRX", "NXT", "SOR"];
66
+ var ORDER_TYPES = {
67
+ "0": "\uBCF4\uD1B5(\uC9C0\uC815\uAC00/limit)",
68
+ "3": "\uC2DC\uC7A5\uAC00(market)",
69
+ "5": "\uC870\uAC74\uBD80\uC9C0\uC815\uAC00",
70
+ "6": "\uCD5C\uC720\uB9AC\uC9C0\uC815\uAC00",
71
+ "7": "\uCD5C\uC6B0\uC120\uC9C0\uC815\uAC00",
72
+ "10": "\uBCF4\uD1B5(IOC)",
73
+ "13": "\uC2DC\uC7A5\uAC00(IOC)",
74
+ "16": "\uCD5C\uC720\uB9AC(IOC)",
75
+ "20": "\uBCF4\uD1B5(FOK)",
76
+ "23": "\uC2DC\uC7A5\uAC00(FOK)",
77
+ "26": "\uCD5C\uC720\uB9AC(FOK)",
78
+ "28": "\uC2A4\uD1B1\uC9C0\uC815\uAC00(stop-limit)",
79
+ "29": "\uC911\uAC04\uAC00",
80
+ "30": "\uC911\uAC04\uAC00(IOC)",
81
+ "31": "\uC911\uAC04\uAC00(FOK)",
82
+ "61": "\uC7A5\uC2DC\uC791\uC804\uC2DC\uAC04\uC678",
83
+ "62": "\uC2DC\uAC04\uC678\uB2E8\uC77C\uAC00",
84
+ "81": "\uC7A5\uB9C8\uAC10\uD6C4\uC2DC\uAC04\uC678"
85
+ };
86
+ var MARKET_ORDER_TYPES = /* @__PURE__ */ new Set(["3", "13", "23"]);
87
+
88
+ // src/config/store.ts
89
+ var fs = __toESM(require("fs"));
90
+ var path = __toESM(require("path"));
91
+ var os = __toESM(require("os"));
92
+ var DEFAULT_CONFIG = { env: "real" };
93
+ function getConfigDir() {
94
+ return path.join(os.homedir(), CONFIG_DIR_NAME);
95
+ }
96
+ function getConfigPath() {
97
+ return path.join(getConfigDir(), CONFIG_FILE_NAME);
98
+ }
99
+ function getTokenPath() {
100
+ return path.join(getConfigDir(), TOKEN_FILE_NAME);
101
+ }
102
+ function ensureConfigDir() {
103
+ const dir = getConfigDir();
104
+ if (!fs.existsSync(dir)) {
105
+ fs.mkdirSync(dir, { mode: 448, recursive: true });
106
+ } else {
107
+ try {
108
+ fs.chmodSync(dir, 448);
109
+ } catch {
110
+ }
111
+ }
112
+ }
113
+ function writeSecure(filePath, data) {
114
+ fs.writeFileSync(filePath, data, { mode: 384 });
115
+ try {
116
+ fs.chmodSync(filePath, 384);
117
+ } catch {
118
+ }
119
+ }
120
+ function loadConfig() {
121
+ try {
122
+ const raw = fs.readFileSync(getConfigPath(), "utf-8");
123
+ return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
124
+ } catch {
125
+ return { ...DEFAULT_CONFIG };
126
+ }
127
+ }
128
+ function getEffectiveConfig() {
129
+ const disk = loadConfig();
130
+ const envVal = process.env[ENV_VARS.env];
131
+ const env = envVal && ENV_ALIASES[envVal.toLowerCase()] ? ENV_ALIASES[envVal.toLowerCase()] : disk.env;
132
+ return {
133
+ env,
134
+ appkey: disk.appkey ?? process.env[ENV_VARS.appkey],
135
+ secretkey: disk.secretkey ?? process.env[ENV_VARS.secretkey]
136
+ };
137
+ }
138
+ function loadTokenCache() {
139
+ try {
140
+ return JSON.parse(fs.readFileSync(getTokenPath(), "utf-8"));
141
+ } catch {
142
+ return {};
143
+ }
144
+ }
145
+ function getCachedToken(env) {
146
+ return loadTokenCache()[env];
147
+ }
148
+ function saveCachedToken(env, token) {
149
+ ensureConfigDir();
150
+ const cache = loadTokenCache();
151
+ cache[env] = token;
152
+ writeSecure(getTokenPath(), JSON.stringify(cache, null, 2));
153
+ }
154
+ function clearCachedToken(env) {
155
+ if (!env) {
156
+ try {
157
+ fs.rmSync(getTokenPath());
158
+ } catch {
159
+ }
160
+ return;
161
+ }
162
+ const cache = loadTokenCache();
163
+ delete cache[env];
164
+ try {
165
+ ensureConfigDir();
166
+ writeSecure(getTokenPath(), JSON.stringify(cache, null, 2));
167
+ } catch {
168
+ }
169
+ }
170
+
171
+ // src/output/error.ts
172
+ var ActionableError = class extends Error {
173
+ suggestedCommand;
174
+ constructor(message, suggestedCommand) {
175
+ super(message);
176
+ this.name = "ActionableError";
177
+ this.suggestedCommand = suggestedCommand;
178
+ }
179
+ };
180
+ var KiwoomApiError = class extends Error {
181
+ returnCode;
182
+ returnMsg;
183
+ apiId;
184
+ constructor(returnCode, returnMsg, apiId) {
185
+ super(`[${returnCode}] ${returnMsg}`);
186
+ this.name = "KiwoomApiError";
187
+ this.returnCode = returnCode;
188
+ this.returnMsg = returnMsg;
189
+ this.apiId = apiId;
190
+ }
191
+ };
192
+
193
+ // src/utils/helpers.ts
194
+ function normalizeStockCode(code) {
195
+ const trimmed = code.trim().toUpperCase();
196
+ const stripped = trimmed.replace(/^A(?=\d)/, "");
197
+ const base = stripped.split("_")[0];
198
+ if (!/^\d{6}$/.test(base)) {
199
+ throw new ActionableError(
200
+ `Invalid stock code "${code}". Expected a 6-digit code like 005930 (\uC0BC\uC131\uC804\uC790).`
201
+ );
202
+ }
203
+ return base;
204
+ }
205
+ function parseKiwoomExpiry(expiresDt) {
206
+ const m = expiresDt.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/);
207
+ if (!m) return NaN;
208
+ const [, y, mo, d, h, mi, s] = m;
209
+ return Date.parse(`${y}-${mo}-${d}T${h}:${mi}:${s}+09:00`);
210
+ }
211
+ function isTokenExpired(expiresDt, bufferMs = 0) {
212
+ const expiry = parseKiwoomExpiry(expiresDt);
213
+ if (Number.isNaN(expiry)) return true;
214
+ return Date.now() + bufferMs >= expiry;
215
+ }
216
+ function todayKst() {
217
+ const now = new Date(Date.now() + 9 * 3600 * 1e3);
218
+ return now.toISOString().slice(0, 10).replace(/-/g, "");
219
+ }
220
+
221
+ // src/client/api-client.ts
222
+ function fetchWithTimeout(url, init) {
223
+ const controller = new AbortController();
224
+ const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
225
+ return fetch(url, { ...init, signal: controller.signal }).finally(
226
+ () => clearTimeout(timeoutId)
227
+ );
228
+ }
229
+ var KiwoomClient = class {
230
+ env;
231
+ baseUrl;
232
+ appkey;
233
+ secretkey;
234
+ tokenOverride;
235
+ memoToken;
236
+ constructor(opts) {
237
+ this.env = opts.env;
238
+ this.baseUrl = BASE_URLS[opts.env];
239
+ if (!this.baseUrl) {
240
+ throw new Error(`Invalid environment: ${opts.env}. Use: real, mock`);
241
+ }
242
+ this.appkey = opts.appkey;
243
+ this.secretkey = opts.secretkey;
244
+ this.tokenOverride = opts.token;
245
+ }
246
+ getEnv() {
247
+ return this.env;
248
+ }
249
+ /** Ensure a valid token exists (issuing + caching as needed) and return it. */
250
+ async authenticate() {
251
+ return this.getToken();
252
+ }
253
+ // ─── OAuth2 ───────────────────────────────────────────────────────────────
254
+ /** Issue a fresh access token from the app key + secret key. */
255
+ async issueToken() {
256
+ if (!this.appkey || !this.secretkey) {
257
+ throw new ActionableError(
258
+ "App key and secret key are required to issue a token.",
259
+ "kiwoom-cli config init"
260
+ );
261
+ }
262
+ const res = await fetchWithTimeout(`${this.baseUrl}${TOKEN_PATH}`, {
263
+ method: "POST",
264
+ headers: { "Content-Type": "application/json;charset=UTF-8" },
265
+ body: JSON.stringify({
266
+ grant_type: "client_credentials",
267
+ appkey: this.appkey,
268
+ secretkey: this.secretkey
269
+ })
270
+ });
271
+ const body = await res.json().catch(() => ({}));
272
+ if (!res.ok || body.return_code !== 0 || !body.token) {
273
+ throw new KiwoomApiError(
274
+ body.return_code ?? res.status,
275
+ body.return_msg ?? `Token request failed (HTTP ${res.status})`
276
+ );
277
+ }
278
+ return { token: body.token, expiresDt: body.expires_dt };
279
+ }
280
+ /** Revoke an access token (defaults to the cached/override token). */
281
+ async revokeToken(token) {
282
+ if (!this.appkey || !this.secretkey) {
283
+ throw new ActionableError(
284
+ "App key and secret key are required to revoke a token.",
285
+ "kiwoom-cli config init"
286
+ );
287
+ }
288
+ const target = token ?? this.tokenOverride ?? getCachedToken(this.env)?.token;
289
+ if (!target) {
290
+ throw new ActionableError("No token to revoke.");
291
+ }
292
+ const res = await fetchWithTimeout(`${this.baseUrl}${REVOKE_PATH}`, {
293
+ method: "POST",
294
+ headers: { "Content-Type": "application/json;charset=UTF-8" },
295
+ body: JSON.stringify({
296
+ appkey: this.appkey,
297
+ secretkey: this.secretkey,
298
+ token: target
299
+ })
300
+ });
301
+ const body = await res.json().catch(() => ({}));
302
+ if (!res.ok || body.return_code !== 0) {
303
+ throw new KiwoomApiError(
304
+ body.return_code ?? res.status,
305
+ body.return_msg ?? `Token revoke failed (HTTP ${res.status})`
306
+ );
307
+ }
308
+ clearCachedToken(this.env);
309
+ return body;
310
+ }
311
+ /**
312
+ * Return a valid token, reusing the cache when possible and issuing+caching a
313
+ * new one when missing or near expiry.
314
+ */
315
+ async getToken() {
316
+ if (this.tokenOverride) return this.tokenOverride;
317
+ if (this.memoToken) return this.memoToken;
318
+ const cached = getCachedToken(this.env);
319
+ const hint = this.appkey?.slice(0, 6);
320
+ if (cached && cached.appkeyHint === hint && !isTokenExpired(cached.expiresDt, TOKEN_REFRESH_BUFFER_MS)) {
321
+ this.memoToken = cached.token;
322
+ return cached.token;
323
+ }
324
+ const { token, expiresDt } = await this.issueToken();
325
+ this.memoToken = token;
326
+ if (hint) {
327
+ saveCachedToken(this.env, { token, expiresDt, appkeyHint: hint });
328
+ }
329
+ return token;
330
+ }
331
+ // ─── Generic TR request ──────────────────────────────────────────────────
332
+ /**
333
+ * Execute a TR. `apiId` is sent in the api-id header; `path` is the category
334
+ * route (e.g. /api/dostk/stkinfo). Returns the parsed body plus pagination.
335
+ */
336
+ async request(apiId, path2, body = {}, options = {}) {
337
+ const send = async (token) => fetchWithTimeout(`${this.baseUrl}${path2}`, {
338
+ method: "POST",
339
+ headers: {
340
+ "Content-Type": "application/json;charset=UTF-8",
341
+ authorization: `Bearer ${token}`,
342
+ "api-id": options.apiId ?? apiId,
343
+ "cont-yn": options.contYn ?? "N",
344
+ "next-key": options.nextKey ?? ""
345
+ },
346
+ body: JSON.stringify(body)
347
+ });
348
+ let res = await send(await this.getToken());
349
+ if (res.status === 401 && !this.tokenOverride) {
350
+ clearCachedToken(this.env);
351
+ this.memoToken = void 0;
352
+ res = await send(await this.getToken());
353
+ }
354
+ const payload = await res.json().catch(() => ({}));
355
+ if (!res.ok) {
356
+ throw new KiwoomApiError(
357
+ payload.return_code ?? res.status,
358
+ payload.return_msg ?? `Request failed (HTTP ${res.status})`,
359
+ apiId
360
+ );
361
+ }
362
+ if (typeof payload.return_code === "number" && payload.return_code !== 0 && !SOFT_EMPTY_RETURN_CODES.has(payload.return_code)) {
363
+ throw new KiwoomApiError(payload.return_code, payload.return_msg ?? "Error", apiId);
364
+ }
365
+ return {
366
+ data: payload,
367
+ contYn: res.headers.get("cont-yn") === "Y",
368
+ nextKey: res.headers.get("next-key") ?? ""
369
+ };
370
+ }
371
+ /**
372
+ * Execute an endpoint from the registry. When `paginate` is set and the
373
+ * endpoint declares a listKey, all pages are fetched and concatenated.
374
+ */
375
+ async callEndpoint(def, body = {}, opts = {}) {
376
+ if (opts.paginate && def.listKey) {
377
+ const data = await this.requestAll(def.apiId, def.path, body, def.listKey);
378
+ return { data, contYn: false, nextKey: "" };
379
+ }
380
+ return this.request(def.apiId, def.path, body, {
381
+ contYn: opts.contYn,
382
+ nextKey: opts.nextKey
383
+ });
384
+ }
385
+ /**
386
+ * Fetch all pages of a TR, concatenating the array under `listKey`.
387
+ * Caps at `maxPages` to avoid runaway loops.
388
+ */
389
+ async requestAll(apiId, path2, body, listKey, maxPages = 20) {
390
+ let page = await this.request(apiId, path2, body);
391
+ const acc = Array.isArray(page.data[listKey]) ? [...page.data[listKey]] : [];
392
+ let pages = 1;
393
+ while (page.contYn && page.nextKey && pages < maxPages) {
394
+ page = await this.request(apiId, path2, body, {
395
+ contYn: "Y",
396
+ nextKey: page.nextKey
397
+ });
398
+ if (Array.isArray(page.data[listKey])) acc.push(...page.data[listKey]);
399
+ pages += 1;
400
+ }
401
+ return { ...page.data, [listKey]: acc };
402
+ }
403
+ };
404
+
405
+ // src/mcp/helpers.ts
406
+ function mcpText(text) {
407
+ return { content: [{ type: "text", text }] };
408
+ }
409
+ function mcpJson(data) {
410
+ return mcpText(JSON.stringify(data, null, 2));
411
+ }
412
+ function mcpError(message) {
413
+ return {
414
+ content: [{ type: "text", text: `ERROR: ${message}` }],
415
+ isError: true
416
+ };
417
+ }
418
+ function clientOrThrow() {
419
+ const config = getEffectiveConfig();
420
+ if (!config.appkey || !config.secretkey) {
421
+ throw new Error("App key / secret key not configured. Run: kiwoom-cli config init");
422
+ }
423
+ return new KiwoomClient({
424
+ env: config.env,
425
+ appkey: config.appkey,
426
+ secretkey: config.secretkey
427
+ });
428
+ }
429
+ function currentEnv() {
430
+ return getEffectiveConfig().env;
431
+ }
432
+ function tool(server2, name, config, handler) {
433
+ server2.registerTool(name, config, handler);
434
+ }
435
+ async function withErrorHandling(fn) {
436
+ try {
437
+ return await fn();
438
+ } catch (err) {
439
+ const message = err instanceof Error ? err.message : String(err);
440
+ return mcpError(message.slice(0, 500));
441
+ }
442
+ }
443
+
444
+ // src/client/endpoints.ts
445
+ var PATHS = {
446
+ stkinfo: "/api/dostk/stkinfo",
447
+ mrkcond: "/api/dostk/mrkcond",
448
+ chart: "/api/dostk/chart",
449
+ acnt: "/api/dostk/acnt",
450
+ ordr: "/api/dostk/ordr",
451
+ crdordr: "/api/dostk/crdordr",
452
+ rkinfo: "/api/dostk/rkinfo",
453
+ sect: "/api/dostk/sect"
454
+ };
455
+ var ENDPOINTS = {
456
+ // ── Stock info (종목정보) ──────────────────────────────────────────────────
457
+ stockInfo: { apiId: "ka10001", path: PATHS.stkinfo, korean: "\uC8FC\uC2DD\uAE30\uBCF8\uC815\uBCF4\uC694\uCCAD" },
458
+ tradingMembers: { apiId: "ka10002", path: PATHS.stkinfo, korean: "\uC8FC\uC2DD\uAC70\uB798\uC6D0\uC694\uCCAD" },
459
+ stockTrades: { apiId: "ka10003", path: PATHS.stkinfo, korean: "\uCCB4\uACB0\uC815\uBCF4\uC694\uCCAD", listKey: "cntr_infr" },
460
+ watchlist: { apiId: "ka10095", path: PATHS.stkinfo, korean: "\uAD00\uC2EC\uC885\uBAA9\uC815\uBCF4\uC694\uCCAD", listKey: "atn_stk_infr" },
461
+ stockList: { apiId: "ka10099", path: PATHS.stkinfo, korean: "\uC885\uBAA9\uC815\uBCF4 \uB9AC\uC2A4\uD2B8", listKey: "list" },
462
+ stockInfoSingle: { apiId: "ka10100", path: PATHS.stkinfo, korean: "\uC885\uBAA9\uC815\uBCF4 \uC870\uD68C" },
463
+ sectorCodeList: { apiId: "ka10101", path: PATHS.stkinfo, korean: "\uC5C5\uC885\uCF54\uB4DC \uB9AC\uC2A4\uD2B8", listKey: "list" },
464
+ creditTrend: { apiId: "ka10013", path: PATHS.stkinfo, korean: "\uC2E0\uC6A9\uB9E4\uB9E4\uB3D9\uD5A5\uC694\uCCAD", listKey: "crd_trde_trend" },
465
+ // ── Market quote (시세) ────────────────────────────────────────────────────
466
+ orderbook: { apiId: "ka10004", path: PATHS.mrkcond, korean: "\uC8FC\uC2DD\uD638\uAC00\uC694\uCCAD" },
467
+ quoteSnapshot: { apiId: "ka10006", path: PATHS.mrkcond, korean: "\uC8FC\uC2DD\uC2DC\uBD84\uC694\uCCAD" },
468
+ priceTableInfo: { apiId: "ka10007", path: PATHS.mrkcond, korean: "\uC2DC\uC138\uD45C\uC131\uC815\uBCF4\uC694\uCCAD" },
469
+ dailyPrice: { apiId: "ka10086", path: PATHS.mrkcond, korean: "\uC77C\uBCC4\uC8FC\uAC00\uC694\uCCAD", listKey: "daly_stkpc" },
470
+ instTradedStocks: { apiId: "ka10044", path: PATHS.mrkcond, korean: "\uC77C\uBCC4\uAE30\uAD00\uB9E4\uB9E4\uC885\uBAA9\uC694\uCCAD", listKey: "daly_orgn_trde_stk" },
471
+ instForeignTrend: { apiId: "ka10045", path: PATHS.mrkcond, korean: "\uC885\uBAA9\uBCC4\uAE30\uAD00\uB9E4\uB9E4\uCD94\uC774\uC694\uCCAD", listKey: "stk_orgn_trde_trnsn" },
472
+ strengthByTime: { apiId: "ka10046", path: PATHS.mrkcond, korean: "\uCCB4\uACB0\uAC15\uB3C4\uC2DC\uAC04\uBCC4\uC694\uCCAD", listKey: "cntr_str_tm" },
473
+ strengthByDay: { apiId: "ka10047", path: PATHS.mrkcond, korean: "\uCCB4\uACB0\uAC15\uB3C4\uC77C\uBCC4\uC694\uCCAD", listKey: "cntr_str_daly" },
474
+ afterHoursOrderbook: { apiId: "ka10087", path: PATHS.mrkcond, korean: "\uC2DC\uAC04\uC678\uB2E8\uC77C\uAC00\uC694\uCCAD" },
475
+ // ── Chart (차트) ───────────────────────────────────────────────────────────
476
+ tickChart: { apiId: "ka10079", path: PATHS.chart, korean: "\uC8FC\uC2DD\uD2F1\uCC28\uD2B8\uC870\uD68C", listKey: "stk_tic_chart_qry" },
477
+ minuteChart: { apiId: "ka10080", path: PATHS.chart, korean: "\uC8FC\uC2DD\uBD84\uBD09\uCC28\uD2B8\uC870\uD68C", listKey: "stk_min_pole_chart_qry" },
478
+ dailyChart: { apiId: "ka10081", path: PATHS.chart, korean: "\uC8FC\uC2DD\uC77C\uBD09\uCC28\uD2B8\uC870\uD68C", listKey: "stk_dt_pole_chart_qry" },
479
+ // NOTE: weekly list key is the doubled "stk_stk_..." — verified live, not a typo.
480
+ weeklyChart: { apiId: "ka10082", path: PATHS.chart, korean: "\uC8FC\uC2DD\uC8FC\uBD09\uCC28\uD2B8\uC870\uD68C", listKey: "stk_stk_pole_chart_qry" },
481
+ monthlyChart: { apiId: "ka10083", path: PATHS.chart, korean: "\uC8FC\uC2DD\uC6D4\uBD09\uCC28\uD2B8\uC870\uD68C", listKey: "stk_mth_pole_chart_qry" },
482
+ yearlyChart: { apiId: "ka10094", path: PATHS.chart, korean: "\uC8FC\uC2DD\uB144\uBD09\uCC28\uD2B8\uC870\uD68C", listKey: "stk_yr_pole_chart_qry" },
483
+ // ── Account (계좌) ─────────────────────────────────────────────────────────
484
+ balance: { apiId: "kt00018", path: PATHS.acnt, korean: "\uACC4\uC88C\uD3C9\uAC00\uC794\uACE0\uB0B4\uC5ED\uC694\uCCAD", listKey: "acnt_evlt_remn_indv_tot" },
485
+ deposit: { apiId: "kt00001", path: PATHS.acnt, korean: "\uC608\uC218\uAE08\uC0C1\uC138\uD604\uD669\uC694\uCCAD", listKey: "stk_entr_prst" },
486
+ evalStatus: { apiId: "kt00004", path: PATHS.acnt, korean: "\uACC4\uC88C\uD3C9\uAC00\uD604\uD669\uC694\uCCAD", listKey: "stk_acnt_evlt_prst" },
487
+ settledBalance: { apiId: "kt00005", path: PATHS.acnt, korean: "\uCCB4\uACB0\uC794\uACE0\uC694\uCCAD", listKey: "stk_cntr_remn" },
488
+ orderDetail: { apiId: "kt00007", path: PATHS.acnt, korean: "\uACC4\uC88C\uBCC4\uC8FC\uBB38\uCCB4\uACB0\uB0B4\uC5ED\uC0C1\uC138\uC694\uCCAD", listKey: "acnt_ord_cntr_prps_dtl" },
489
+ orderStatus: { apiId: "kt00009", path: PATHS.acnt, korean: "\uACC4\uC88C\uBCC4\uC8FC\uBB38\uCCB4\uACB0\uD604\uD669\uC694\uCCAD", listKey: "acnt_ord_cntr_prst" },
490
+ openOrders: { apiId: "ka10075", path: PATHS.acnt, korean: "\uBBF8\uCCB4\uACB0\uC694\uCCAD", listKey: "oso" },
491
+ executions: { apiId: "ka10076", path: PATHS.acnt, korean: "\uCCB4\uACB0\uC694\uCCAD", listKey: "cntr" },
492
+ realizedPlByDate: { apiId: "ka10072", path: PATHS.acnt, korean: "\uC77C\uC790\uBCC4\uC885\uBAA9\uBCC4\uC2E4\uD604\uC190\uC775\uC694\uCCAD_\uC77C\uC790", listKey: "dt_stk_div_rlzt_pl" },
493
+ realizedPlByPeriod: { apiId: "ka10073", path: PATHS.acnt, korean: "\uC77C\uC790\uBCC4\uC885\uBAA9\uBCC4\uC2E4\uD604\uC190\uC775\uC694\uCCAD_\uAE30\uAC04", listKey: "dt_stk_rlzt_pl" },
494
+ tradeJournal: { apiId: "ka10170", path: PATHS.acnt, korean: "\uB2F9\uC77C\uB9E4\uB9E4\uC77C\uC9C0\uC694\uCCAD", listKey: "tdy_trde_diary" },
495
+ dailyReturn: { apiId: "kt00016", path: PATHS.acnt, korean: "\uC77C\uBCC4\uACC4\uC88C\uC218\uC775\uB960\uC0C1\uC138\uD604\uD669\uC694\uCCAD" },
496
+ // ── Order (주문) — WRITE, real money ──────────────────────────────────────
497
+ buy: { apiId: "kt10000", path: PATHS.ordr, korean: "\uC8FC\uC2DD \uB9E4\uC218\uC8FC\uBB38", isWrite: true },
498
+ sell: { apiId: "kt10001", path: PATHS.ordr, korean: "\uC8FC\uC2DD \uB9E4\uB3C4\uC8FC\uBB38", isWrite: true },
499
+ modify: { apiId: "kt10002", path: PATHS.ordr, korean: "\uC8FC\uC2DD \uC815\uC815\uC8FC\uBB38", isWrite: true },
500
+ cancel: { apiId: "kt10003", path: PATHS.ordr, korean: "\uC8FC\uC2DD \uCDE8\uC18C\uC8FC\uBB38", isWrite: true },
501
+ creditBuy: { apiId: "kt10006", path: PATHS.crdordr, korean: "\uC2E0\uC6A9 \uB9E4\uC218\uC8FC\uBB38", isWrite: true },
502
+ creditSell: { apiId: "kt10007", path: PATHS.crdordr, korean: "\uC2E0\uC6A9 \uB9E4\uB3C4\uC8FC\uBB38", isWrite: true },
503
+ creditModify: { apiId: "kt10008", path: PATHS.crdordr, korean: "\uC2E0\uC6A9 \uC815\uC815\uC8FC\uBB38", isWrite: true },
504
+ creditCancel: { apiId: "kt10009", path: PATHS.crdordr, korean: "\uC2E0\uC6A9 \uCDE8\uC18C\uC8FC\uBB38", isWrite: true },
505
+ // ── Ranking (순위정보) ─────────────────────────────────────────────────────
506
+ rankFluctuation: { apiId: "ka10027", path: PATHS.rkinfo, korean: "\uC804\uC77C\uB300\uBE44\uB4F1\uB77D\uB960\uC21C\uC704\uC694\uCCAD", listKey: "pred_pre_flu_rt_upper" },
507
+ rankVolume: { apiId: "ka10030", path: PATHS.rkinfo, korean: "\uB2F9\uC77C\uAC70\uB798\uB7C9\uC0C1\uC704\uC694\uCCAD", listKey: "tdy_trde_qty_upper" },
508
+ rankTradeAmount: { apiId: "ka10032", path: PATHS.rkinfo, korean: "\uAC70\uB798\uB300\uAE08\uC0C1\uC704\uC694\uCCAD", listKey: "trde_prica_upper" },
509
+ rankVolumeSurge: { apiId: "ka10023", path: PATHS.rkinfo, korean: "\uAC70\uB798\uB7C9\uAE09\uC99D\uC694\uCCAD", listKey: "trde_qty_sdnin" },
510
+ rankPrevVolume: { apiId: "ka10031", path: PATHS.rkinfo, korean: "\uC804\uC77C\uAC70\uB798\uB7C9\uC0C1\uC704\uC694\uCCAD", listKey: "pred_trde_qty_upper" },
511
+ // ── Sector / industry (업종) ───────────────────────────────────────────────
512
+ sectorPrice: { apiId: "ka20001", path: PATHS.sect, korean: "\uC5C5\uC885\uD604\uC7AC\uAC00\uC694\uCCAD", listKey: "inds_cur_prc_tm" },
513
+ sectorStocks: { apiId: "ka20002", path: PATHS.sect, korean: "\uC5C5\uC885\uBCC4\uC8FC\uAC00\uC694\uCCAD", listKey: "inds_stkpc" },
514
+ sectorAllIndex: { apiId: "ka20003", path: PATHS.sect, korean: "\uC804\uC5C5\uC885\uC9C0\uC218\uC694\uCCAD", listKey: "all_inds_idex" },
515
+ sectorDaily: { apiId: "ka20009", path: PATHS.sect, korean: "\uC5C5\uC885\uD604\uC7AC\uAC00 \uC77C\uBCC4\uC694\uCCAD", listKey: "inds_cur_prc_daly_rept" }
516
+ };
517
+
518
+ // src/utils/format.ts
519
+ var NUMERIC_RE = /^([+-]*)0*(\d+)(\.\d+)?$/;
520
+ function unpad(value) {
521
+ if (value === null || value === void 0) return "";
522
+ const str = String(value).trim();
523
+ const m = str.match(NUMERIC_RE);
524
+ if (!m) return str;
525
+ const sign = m[1].includes("-") ? "-" : m[1].includes("+") ? "+" : "";
526
+ const intPart = m[2].replace(/^0+(?=\d)/, "");
527
+ return `${sign}${intPart}${m[3] ?? ""}`;
528
+ }
529
+ function formatStamp(value) {
530
+ if (!value) return "";
531
+ const s = String(value).trim();
532
+ if (/^\d{14}$/.test(s)) {
533
+ return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)} ${s.slice(8, 10)}:${s.slice(10, 12)}:${s.slice(12, 14)}`;
534
+ }
535
+ if (/^\d{8}$/.test(s)) {
536
+ return `${s.slice(0, 4)}-${s.slice(4, 6)}-${s.slice(6, 8)}`;
537
+ }
538
+ if (/^\d{6}$/.test(s)) {
539
+ return `${s.slice(0, 2)}:${s.slice(2, 4)}:${s.slice(4, 6)}`;
540
+ }
541
+ return s;
542
+ }
543
+
544
+ // src/utils/orderbook.ts
545
+ function parseOrderbook(d) {
546
+ const asks = [];
547
+ for (let n = 10; n >= 2; n--) {
548
+ asks.push({
549
+ level: n,
550
+ price: unpad(d[`sel_${n}th_pre_bid`]),
551
+ qty: unpad(d[`sel_${n}th_pre_req`])
552
+ });
553
+ }
554
+ asks.push({ level: 1, price: unpad(d.sel_fpr_bid), qty: unpad(d.sel_fpr_req) });
555
+ const bids = [];
556
+ bids.push({ level: 1, price: unpad(d.buy_fpr_bid), qty: unpad(d.buy_fpr_req) });
557
+ for (let n = 2; n <= 10; n++) {
558
+ bids.push({
559
+ level: n,
560
+ price: unpad(d[`buy_${n}th_pre_bid`]),
561
+ qty: unpad(d[`buy_${n}th_pre_req`])
562
+ });
563
+ }
564
+ return {
565
+ baseTime: formatStamp(d.bid_req_base_tm),
566
+ asks,
567
+ bids,
568
+ totalAskQty: unpad(d.tot_sel_req),
569
+ totalBidQty: unpad(d.tot_buy_req)
570
+ };
571
+ }
572
+
573
+ // src/mcp/tools/market.ts
574
+ function registerMarketTools(server2) {
575
+ tool(
576
+ server2,
577
+ "get_stock_info",
578
+ {
579
+ description: "Get Kiwoom stock fundamentals + current price for a Korean stock (name, price, change, PER/EPS/ROE/PBR/BPS, OHLC, limits). ka10001.",
580
+ inputSchema: { code: import_zod.z.string().describe("6-digit stock code, e.g. 005930 (\uC0BC\uC131\uC804\uC790)") }
581
+ },
582
+ async ({ code }) => withErrorHandling(async () => {
583
+ const client = clientOrThrow();
584
+ const { data } = await client.callEndpoint(ENDPOINTS.stockInfo, {
585
+ stk_cd: normalizeStockCode(code)
586
+ });
587
+ return mcpJson(data);
588
+ })
589
+ );
590
+ tool(
591
+ server2,
592
+ "get_price",
593
+ {
594
+ description: "Get a rich current-price snapshot incl. top-of-book for a stock. ka10007.",
595
+ inputSchema: { code: import_zod.z.string().describe("6-digit stock code") }
596
+ },
597
+ async ({ code }) => withErrorHandling(async () => {
598
+ const client = clientOrThrow();
599
+ const { data } = await client.callEndpoint(ENDPOINTS.priceTableInfo, {
600
+ stk_cd: normalizeStockCode(code)
601
+ });
602
+ return mcpJson(data);
603
+ })
604
+ );
605
+ tool(
606
+ server2,
607
+ "get_orderbook",
608
+ {
609
+ description: "Get the 10-level bid/ask order book for a stock, parsed into ordered levels. ka10004.",
610
+ inputSchema: { code: import_zod.z.string().describe("6-digit stock code") }
611
+ },
612
+ async ({ code }) => withErrorHandling(async () => {
613
+ const client = clientOrThrow();
614
+ const stk = normalizeStockCode(code);
615
+ const { data } = await client.callEndpoint(ENDPOINTS.orderbook, { stk_cd: stk });
616
+ return mcpJson({ code: stk, ...parseOrderbook(data) });
617
+ })
618
+ );
619
+ tool(
620
+ server2,
621
+ "get_daily_price",
622
+ {
623
+ description: "Get daily price history ending at a base date. ka10086.",
624
+ inputSchema: {
625
+ code: import_zod.z.string().describe("6-digit stock code"),
626
+ date: import_zod.z.string().optional().describe("Base date YYYYMMDD (default today)")
627
+ }
628
+ },
629
+ async ({ code, date }) => withErrorHandling(async () => {
630
+ const client = clientOrThrow();
631
+ const { data } = await client.callEndpoint(ENDPOINTS.dailyPrice, {
632
+ stk_cd: normalizeStockCode(code),
633
+ qry_dt: date ?? todayKst(),
634
+ indc_tp: "0"
635
+ });
636
+ return mcpJson(data);
637
+ })
638
+ );
639
+ tool(
640
+ server2,
641
+ "get_recent_trades",
642
+ {
643
+ description: "Get recent tick-by-tick executions for a stock. ka10003.",
644
+ inputSchema: { code: import_zod.z.string().describe("6-digit stock code") }
645
+ },
646
+ async ({ code }) => withErrorHandling(async () => {
647
+ const client = clientOrThrow();
648
+ const { data } = await client.callEndpoint(ENDPOINTS.stockTrades, {
649
+ stk_cd: normalizeStockCode(code)
650
+ });
651
+ return mcpJson(data);
652
+ })
653
+ );
654
+ tool(
655
+ server2,
656
+ "search_stocks",
657
+ {
658
+ description: "Search the master stock list by name keyword or code prefix. Returns matching code/name/market. ka10099.",
659
+ inputSchema: {
660
+ keyword: import_zod.z.string().describe("Name substring or code prefix to match"),
661
+ market: import_zod.z.enum(["0", "10"]).optional().describe("0=KOSPI (default), 10=KOSDAQ"),
662
+ limit: import_zod.z.number().min(1).max(200).optional().describe("Max rows (default 30)")
663
+ }
664
+ },
665
+ async ({ keyword, market, limit }) => withErrorHandling(async () => {
666
+ const client = clientOrThrow();
667
+ const { data } = await client.callEndpoint(
668
+ ENDPOINTS.stockList,
669
+ { mrkt_tp: market ?? "0" },
670
+ { paginate: true }
671
+ );
672
+ const rows = Array.isArray(data.list) ? data.list : [];
673
+ const kw = keyword.toLowerCase();
674
+ const matches = rows.filter(
675
+ (r) => String(r.name ?? "").toLowerCase().includes(kw) || String(r.code ?? "").startsWith(keyword)
676
+ ).slice(0, limit ?? 30).map((r) => ({ code: r.code, name: r.name, market: r.marketName, sector: r.upName }));
677
+ return mcpJson(matches);
678
+ })
679
+ );
680
+ }
681
+
682
+ // src/mcp/tools/chart.ts
683
+ var import_zod2 = require("zod");
684
+ var PERIOD_EP = {
685
+ day: ENDPOINTS.dailyChart,
686
+ week: ENDPOINTS.weeklyChart,
687
+ month: ENDPOINTS.monthlyChart,
688
+ year: ENDPOINTS.yearlyChart
689
+ };
690
+ function registerChartTools(server2) {
691
+ tool(
692
+ server2,
693
+ "get_chart",
694
+ {
695
+ description: "Get OHLC chart data for a stock. Period charts (day/week/month/year) end at base date; tick/minute use an aggregation scope. Returns latest-first; use count to cap rows.",
696
+ inputSchema: {
697
+ code: import_zod2.z.string().describe("6-digit stock code"),
698
+ timeframe: import_zod2.z.enum(["tick", "minute", "day", "week", "month", "year"]).describe("Chart timeframe"),
699
+ scope: import_zod2.z.string().optional().describe("Tick units (1/3/5/10/30) or minute interval (1/3/5/10/15/30/45/60); default 1"),
700
+ date: import_zod2.z.string().optional().describe("Base date YYYYMMDD for period charts (default today)"),
701
+ adjusted: import_zod2.z.boolean().optional().describe("Adjust for splits/rights (default true)"),
702
+ count: import_zod2.z.number().min(1).max(900).optional().describe("Max rows to return (default 50)")
703
+ }
704
+ },
705
+ async ({ code, timeframe, scope, date, adjusted, count }) => withErrorHandling(async () => {
706
+ const client = clientOrThrow();
707
+ const stk = normalizeStockCode(code);
708
+ const upd = adjusted === false ? "0" : "1";
709
+ let def;
710
+ let body;
711
+ if (timeframe === "tick") {
712
+ def = ENDPOINTS.tickChart;
713
+ body = { stk_cd: stk, tic_scope: scope ?? "1", upd_stkpc_tp: upd };
714
+ } else if (timeframe === "minute") {
715
+ def = ENDPOINTS.minuteChart;
716
+ body = { stk_cd: stk, tic_scope: scope ?? "1", upd_stkpc_tp: upd };
717
+ } else {
718
+ def = PERIOD_EP[timeframe];
719
+ body = { stk_cd: stk, base_dt: date ?? todayKst(), upd_stkpc_tp: upd };
720
+ }
721
+ const { data } = await client.callEndpoint(def, body);
722
+ const rows = Array.isArray(data[def.listKey]) ? data[def.listKey] : [];
723
+ return mcpJson({
724
+ code: stk,
725
+ timeframe,
726
+ rows: rows.slice(0, count ?? 50)
727
+ });
728
+ })
729
+ );
730
+ }
731
+
732
+ // src/mcp/tools/account.ts
733
+ var import_zod3 = require("zod");
734
+ function registerAccountTools(server2) {
735
+ tool(
736
+ server2,
737
+ "get_balance",
738
+ {
739
+ description: "Get account evaluation balance: totals (purchase/eval/PnL/profit-rate) and per-holding detail. kt00018.",
740
+ inputSchema: {
741
+ exchange: import_zod3.z.enum(["KRX", "NXT", "%"]).optional().describe("Exchange filter (default KRX)")
742
+ }
743
+ },
744
+ async ({ exchange }) => withErrorHandling(async () => {
745
+ const client = clientOrThrow();
746
+ const { data } = await client.callEndpoint(ENDPOINTS.balance, {
747
+ qry_tp: "1",
748
+ dmst_stex_tp: exchange ?? "KRX"
749
+ });
750
+ return mcpJson(data);
751
+ })
752
+ );
753
+ tool(
754
+ server2,
755
+ "get_deposit",
756
+ {
757
+ description: "Get cash deposit detail: cash, orderable/withdrawable, D+2 settlement. kt00001."
758
+ },
759
+ async () => withErrorHandling(async () => {
760
+ const client = clientOrThrow();
761
+ const { data } = await client.callEndpoint(ENDPOINTS.deposit, { qry_tp: "3" });
762
+ return mcpJson(data);
763
+ })
764
+ );
765
+ tool(
766
+ server2,
767
+ "get_open_orders",
768
+ {
769
+ description: "Get open / unfilled orders (optionally for one stock). ka10075.",
770
+ inputSchema: { code: import_zod3.z.string().optional().describe("6-digit stock code to filter") }
771
+ },
772
+ async ({ code }) => withErrorHandling(async () => {
773
+ const client = clientOrThrow();
774
+ const body = code ? { all_stk_tp: "0", trde_tp: "0", stk_cd: normalizeStockCode(code), stex_tp: "0" } : { all_stk_tp: "1", trde_tp: "0", stk_cd: "", stex_tp: "0" };
775
+ const { data } = await client.callEndpoint(ENDPOINTS.openOrders, body);
776
+ return mcpJson(data);
777
+ })
778
+ );
779
+ tool(
780
+ server2,
781
+ "get_executions",
782
+ {
783
+ description: "Get filled executions (optionally for one stock). ka10076.",
784
+ inputSchema: { code: import_zod3.z.string().optional().describe("6-digit stock code to filter") }
785
+ },
786
+ async ({ code }) => withErrorHandling(async () => {
787
+ const client = clientOrThrow();
788
+ const { data } = await client.callEndpoint(ENDPOINTS.executions, {
789
+ stk_cd: code ? normalizeStockCode(code) : "",
790
+ qry_tp: code ? "1" : "0",
791
+ sell_tp: "0",
792
+ ord_no: "",
793
+ stex_tp: "0"
794
+ });
795
+ return mcpJson(data);
796
+ })
797
+ );
798
+ tool(
799
+ server2,
800
+ "get_realized_pnl",
801
+ {
802
+ description: "Get realized profit/loss for a stock over a date (or period, max 3 months). ka10072 / ka10073.",
803
+ inputSchema: {
804
+ code: import_zod3.z.string().describe("6-digit stock code"),
805
+ start: import_zod3.z.string().optional().describe("Start date YYYYMMDD (default today)"),
806
+ end: import_zod3.z.string().optional().describe("End date YYYYMMDD; if set, queries the period")
807
+ }
808
+ },
809
+ async ({ code, start, end }) => withErrorHandling(async () => {
810
+ const client = clientOrThrow();
811
+ const stk = normalizeStockCode(code);
812
+ const { data } = end ? await client.callEndpoint(ENDPOINTS.realizedPlByPeriod, {
813
+ stk_cd: stk,
814
+ strt_dt: start ?? end,
815
+ end_dt: end
816
+ }) : await client.callEndpoint(ENDPOINTS.realizedPlByDate, {
817
+ stk_cd: stk,
818
+ strt_dt: start ?? todayKst()
819
+ });
820
+ return mcpJson(data);
821
+ })
822
+ );
823
+ }
824
+
825
+ // src/mcp/tools/order.ts
826
+ var import_zod4 = require("zod");
827
+ function resolveType(type, price) {
828
+ const trde_tp = type ?? (price ? "0" : "3");
829
+ if (!ORDER_TYPES[trde_tp]) {
830
+ throw new Error(`Invalid order type ${trde_tp}. Known: ${Object.keys(ORDER_TYPES).join(", ")}`);
831
+ }
832
+ if (MARKET_ORDER_TYPES.has(trde_tp)) {
833
+ if (price) throw new Error("Market orders must not include a price.");
834
+ return { trde_tp, ord_uv: "" };
835
+ }
836
+ if (!price) throw new Error(`Order type ${trde_tp} requires a price.`);
837
+ return { trde_tp, ord_uv: price };
838
+ }
839
+ function registerOrderTools(server2) {
840
+ tool(
841
+ server2,
842
+ "place_order",
843
+ {
844
+ description: 'Place a buy or sell order. SAFETY: nothing is sent unless confirm=true; otherwise a preview is returned. On the "real" environment this trades REAL money. kt10000/kt10001 (or credit kt10006/kt10007).',
845
+ inputSchema: {
846
+ side: import_zod4.z.enum(["buy", "sell"]).describe("buy or sell"),
847
+ code: import_zod4.z.string().describe("6-digit stock code"),
848
+ qty: import_zod4.z.string().describe("Order quantity"),
849
+ price: import_zod4.z.string().optional().describe("Limit price (won); omit for a market order"),
850
+ type: import_zod4.z.string().optional().describe("Order type code trde_tp (default 0=limit / 3=market)"),
851
+ condPrice: import_zod4.z.string().optional().describe("Trigger price for stop/conditional types (e.g. 28)"),
852
+ exchange: import_zod4.z.enum(["KRX", "NXT", "SOR"]).optional().describe("Exchange routing (default KRX)"),
853
+ credit: import_zod4.z.boolean().optional().describe("Use a credit (margin) order"),
854
+ creditDeal: import_zod4.z.enum(["33", "99"]).optional().describe("Credit deal type (sell): 33=\uC735\uC790, 99=\uC735\uC790\uD569"),
855
+ loanDate: import_zod4.z.string().optional().describe("Credit loan date YYYYMMDD (some credit sells)"),
856
+ confirm: import_zod4.z.boolean().optional().describe("Must be true to actually submit")
857
+ }
858
+ },
859
+ async ({ side, code, qty, price, type, condPrice, exchange, credit, creditDeal, loanDate, confirm }) => withErrorHandling(async () => {
860
+ const stk = normalizeStockCode(code);
861
+ const { trde_tp, ord_uv } = resolveType(type, price);
862
+ if (trde_tp === "28" && !condPrice) {
863
+ throw new Error("Stop-limit orders (type 28) require condPrice.");
864
+ }
865
+ const ex = exchange ?? "KRX";
866
+ if (!ORDER_EXCHANGE_TYPES.includes(ex)) throw new Error(`Invalid exchange ${ex}`);
867
+ const def = credit ? side === "buy" ? ENDPOINTS.creditBuy : ENDPOINTS.creditSell : side === "buy" ? ENDPOINTS.buy : ENDPOINTS.sell;
868
+ const body = {
869
+ dmst_stex_tp: ex,
870
+ stk_cd: stk,
871
+ ord_qty: qty,
872
+ ord_uv,
873
+ trde_tp,
874
+ cond_uv: condPrice ?? ""
875
+ };
876
+ if (credit && side === "sell") {
877
+ body.crd_deal_tp = creditDeal ?? "33";
878
+ if (loanDate) body.crd_loan_dt = loanDate;
879
+ }
880
+ const preview = {
881
+ willSubmit: confirm === true,
882
+ env: currentEnv(),
883
+ apiId: def.apiId,
884
+ side,
885
+ credit: Boolean(credit),
886
+ stock: stk,
887
+ qty,
888
+ price: MARKET_ORDER_TYPES.has(trde_tp) ? "MARKET" : ord_uv,
889
+ orderType: `${trde_tp} (${ORDER_TYPES[trde_tp]})`,
890
+ exchange: ex
891
+ };
892
+ if (confirm !== true) {
893
+ return mcpText(
894
+ `PREVIEW ONLY \u2014 set confirm=true to submit.
895
+ ${JSON.stringify(preview, null, 2)}`
896
+ );
897
+ }
898
+ const { data } = await clientOrThrow().callEndpoint(def, body);
899
+ return mcpJson({ submitted: true, ...preview, result: data });
900
+ })
901
+ );
902
+ tool(
903
+ server2,
904
+ "modify_order",
905
+ {
906
+ description: "Modify a resting order (new qty/price). Nothing is sent unless confirm=true. kt10002/kt10008.",
907
+ inputSchema: {
908
+ orderNo: import_zod4.z.string().describe("Original order number"),
909
+ code: import_zod4.z.string().describe("6-digit stock code"),
910
+ qty: import_zod4.z.string().describe("New quantity"),
911
+ price: import_zod4.z.string().describe("New price"),
912
+ condPrice: import_zod4.z.string().optional().describe("New trigger price for conditional/stop orders"),
913
+ exchange: import_zod4.z.enum(["KRX", "NXT", "SOR"]).optional().describe("Exchange (default KRX)"),
914
+ credit: import_zod4.z.boolean().optional(),
915
+ confirm: import_zod4.z.boolean().optional().describe("Must be true to actually submit")
916
+ }
917
+ },
918
+ async ({ orderNo, code, qty, price, condPrice, exchange, credit, confirm }) => withErrorHandling(async () => {
919
+ const stk = normalizeStockCode(code);
920
+ const ex = exchange ?? "KRX";
921
+ const def = credit ? ENDPOINTS.creditModify : ENDPOINTS.modify;
922
+ const body = {
923
+ dmst_stex_tp: ex,
924
+ orig_ord_no: orderNo,
925
+ stk_cd: stk,
926
+ mdfy_qty: qty,
927
+ mdfy_uv: price,
928
+ mdfy_cond_uv: condPrice ?? ""
929
+ };
930
+ const preview = { willSubmit: confirm === true, env: currentEnv(), apiId: def.apiId, orderNo, stock: stk, qty, price, exchange: ex };
931
+ if (confirm !== true) {
932
+ return mcpText(`PREVIEW ONLY \u2014 set confirm=true to submit.
933
+ ${JSON.stringify(preview, null, 2)}`);
934
+ }
935
+ const { data } = await clientOrThrow().callEndpoint(def, body);
936
+ return mcpJson({ submitted: true, ...preview, result: data });
937
+ })
938
+ );
939
+ tool(
940
+ server2,
941
+ "cancel_order",
942
+ {
943
+ description: "Cancel a resting order (qty 0 = all). Nothing is sent unless confirm=true. kt10003/kt10009.",
944
+ inputSchema: {
945
+ orderNo: import_zod4.z.string().describe("Original order number"),
946
+ code: import_zod4.z.string().describe("6-digit stock code"),
947
+ qty: import_zod4.z.string().optional().describe("Quantity to cancel; 0 = all remaining (default 0)"),
948
+ exchange: import_zod4.z.enum(["KRX", "NXT", "SOR"]).optional().describe("Exchange (default KRX)"),
949
+ credit: import_zod4.z.boolean().optional(),
950
+ confirm: import_zod4.z.boolean().optional().describe("Must be true to actually submit")
951
+ }
952
+ },
953
+ async ({ orderNo, code, qty, exchange, credit, confirm }) => withErrorHandling(async () => {
954
+ const stk = normalizeStockCode(code);
955
+ const ex = exchange ?? "KRX";
956
+ const def = credit ? ENDPOINTS.creditCancel : ENDPOINTS.cancel;
957
+ const body = { dmst_stex_tp: ex, orig_ord_no: orderNo, stk_cd: stk, cncl_qty: qty ?? "0" };
958
+ const preview = { willSubmit: confirm === true, env: currentEnv(), apiId: def.apiId, orderNo, stock: stk, qty: qty ?? "0 (all)", exchange: ex };
959
+ if (confirm !== true) {
960
+ return mcpText(`PREVIEW ONLY \u2014 set confirm=true to submit.
961
+ ${JSON.stringify(preview, null, 2)}`);
962
+ }
963
+ const { data } = await clientOrThrow().callEndpoint(def, body);
964
+ return mcpJson({ submitted: true, ...preview, result: data });
965
+ })
966
+ );
967
+ }
968
+
969
+ // src/mcp/tools/ranking.ts
970
+ var import_zod5 = require("zod");
971
+ function registerRankingTools(server2) {
972
+ tool(
973
+ server2,
974
+ "get_ranking",
975
+ {
976
+ description: "Get a market ranking list: gainers/losers (fluctuation), today volume, trade value (amount), volume surge, or previous-day volume.",
977
+ inputSchema: {
978
+ kind: import_zod5.z.enum(["fluctuation", "volume", "amount", "surge", "prev-volume"]).describe("Ranking type"),
979
+ market: import_zod5.z.enum(["000", "001", "101"]).optional().describe("000=all, 001=KOSPI, 101=KOSDAQ"),
980
+ sort: import_zod5.z.string().optional().describe("Sort code (meaning varies per kind; default 1)"),
981
+ exchange: import_zod5.z.enum(["1", "2", "3"]).optional().describe("1=KRX, 2=NXT, 3=unified (default 3)")
982
+ }
983
+ },
984
+ async ({ kind, market, sort, exchange }) => withErrorHandling(async () => {
985
+ const mrkt_tp = market ?? "000";
986
+ const stex_tp = exchange ?? "3";
987
+ let def;
988
+ let body;
989
+ switch (kind) {
990
+ case "fluctuation":
991
+ def = ENDPOINTS.rankFluctuation;
992
+ body = {
993
+ mrkt_tp,
994
+ sort_tp: sort ?? "1",
995
+ trde_qty_cnd: "0000",
996
+ stk_cnd: "0",
997
+ crd_cnd: "0",
998
+ updown_incls: "1",
999
+ pric_cnd: "0",
1000
+ trde_prica_cnd: "0",
1001
+ stex_tp
1002
+ };
1003
+ break;
1004
+ case "volume":
1005
+ def = ENDPOINTS.rankVolume;
1006
+ body = {
1007
+ mrkt_tp,
1008
+ sort_tp: sort ?? "1",
1009
+ mang_stk_incls: "0",
1010
+ crd_tp: "0",
1011
+ trde_qty_tp: "0",
1012
+ pric_tp: "0",
1013
+ trde_prica_tp: "0",
1014
+ mrkt_open_tp: "0",
1015
+ stex_tp
1016
+ };
1017
+ break;
1018
+ case "amount":
1019
+ def = ENDPOINTS.rankTradeAmount;
1020
+ body = { mrkt_tp, mang_stk_incls: "1", stex_tp };
1021
+ break;
1022
+ case "surge":
1023
+ def = ENDPOINTS.rankVolumeSurge;
1024
+ body = {
1025
+ mrkt_tp,
1026
+ sort_tp: sort ?? "1",
1027
+ tm_tp: "2",
1028
+ trde_qty_tp: "5",
1029
+ tm: "",
1030
+ stk_cnd: "0",
1031
+ pric_tp: "0",
1032
+ stex_tp
1033
+ };
1034
+ break;
1035
+ default:
1036
+ def = ENDPOINTS.rankPrevVolume;
1037
+ body = { mrkt_tp, qry_tp: "1", rank_strt: "0", rank_end: "100", stex_tp };
1038
+ }
1039
+ const { data } = await clientOrThrow().callEndpoint(def, body);
1040
+ return mcpJson(data);
1041
+ })
1042
+ );
1043
+ tool(
1044
+ server2,
1045
+ "get_sector",
1046
+ {
1047
+ description: "Get sector/industry data: index price, constituent stock prices, all sector indices, or daily index history.",
1048
+ inputSchema: {
1049
+ kind: import_zod5.z.enum(["price", "stocks", "all", "daily"]).describe("Sector query type"),
1050
+ market: import_zod5.z.enum(["0", "1", "2"]).optional().describe("0=KOSPI, 1=KOSDAQ, 2=KOSPI200"),
1051
+ code: import_zod5.z.string().optional().describe("Industry code inds_cd (default 001=\uC885\uD569KOSPI)")
1052
+ }
1053
+ },
1054
+ async ({ kind, market, code }) => withErrorHandling(async () => {
1055
+ const mrkt_tp = market ?? "0";
1056
+ const inds_cd = code ?? "001";
1057
+ let def;
1058
+ let body;
1059
+ switch (kind) {
1060
+ case "stocks":
1061
+ def = ENDPOINTS.sectorStocks;
1062
+ body = { mrkt_tp, inds_cd, stex_tp: "1" };
1063
+ break;
1064
+ case "all":
1065
+ def = ENDPOINTS.sectorAllIndex;
1066
+ body = { inds_cd };
1067
+ break;
1068
+ case "daily":
1069
+ def = ENDPOINTS.sectorDaily;
1070
+ body = { mrkt_tp, inds_cd };
1071
+ break;
1072
+ default:
1073
+ def = ENDPOINTS.sectorPrice;
1074
+ body = { mrkt_tp, inds_cd };
1075
+ }
1076
+ const { data } = await clientOrThrow().callEndpoint(def, body);
1077
+ return mcpJson(data);
1078
+ })
1079
+ );
1080
+ }
1081
+
1082
+ // src/mcp.ts
1083
+ var server = new import_mcp.McpServer({
1084
+ name: "kiwoom-mcp",
1085
+ version: "0.1.0"
1086
+ });
1087
+ registerMarketTools(server);
1088
+ registerChartTools(server);
1089
+ registerAccountTools(server);
1090
+ registerOrderTools(server);
1091
+ registerRankingTools(server);
1092
+ var transport = new import_stdio.StdioServerTransport();
1093
+ server.connect(transport).catch((err) => {
1094
+ console.error("MCP server error:", err);
1095
+ process.exit(1);
1096
+ });
1097
+ //# sourceMappingURL=mcp.js.map