kiwoom-mcp-server 0.8.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/LICENSE +21 -0
- package/README.en.md +227 -0
- package/README.md +217 -0
- package/dist/config.js +61 -0
- package/dist/context.js +16 -0
- package/dist/index.js +15 -0
- package/dist/isa/classify-etf.js +84 -0
- package/dist/isa/classify.js +198 -0
- package/dist/isa/realized.js +55 -0
- package/dist/isa/tax.js +53 -0
- package/dist/kiwoom/api.js +372 -0
- package/dist/kiwoom/auth.js +89 -0
- package/dist/kiwoom/client.js +104 -0
- package/dist/kiwoom/errors.js +15 -0
- package/dist/kiwoom/master-list.js +25 -0
- package/dist/kiwoom/types.js +385 -0
- package/dist/server.js +54 -0
- package/dist/tools/account-balance.js +36 -0
- package/dist/tools/account-holdings.js +52 -0
- package/dist/tools/etf-info.js +44 -0
- package/dist/tools/foreign-holding.js +61 -0
- package/dist/tools/helpers.js +19 -0
- package/dist/tools/investor-trend.js +77 -0
- package/dist/tools/isa-tax-status.js +245 -0
- package/dist/tools/market-index.js +41 -0
- package/dist/tools/orderbook.js +50 -0
- package/dist/tools/pending-orders.js +67 -0
- package/dist/tools/ping.js +14 -0
- package/dist/tools/ranking.js +83 -0
- package/dist/tools/short-selling.js +66 -0
- package/dist/tools/stock-chart.js +94 -0
- package/dist/tools/stock-price.js +66 -0
- package/dist/tools/stock-search.js +63 -0
- package/dist/tools/theme.js +120 -0
- package/dist/tools/trading-journal.js +61 -0
- package/dist/tools/transactions.js +80 -0
- package/dist/tools/watchlist.js +115 -0
- package/dist/utils/date.js +15 -0
- package/dist/utils/num.js +67 -0
- package/dist/utils/redact.js +14 -0
- package/dist/utils/sleep.js +3 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ChunSam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# kiwoom-mcp-server
|
|
2
|
+
|
|
3
|
+
[한국어](README.md) · **English**
|
|
4
|
+
|
|
5
|
+
A **read-only** MCP server that exposes the Kiwoom Securities REST API.
|
|
6
|
+
From Claude Desktop / Claude Code you can query Korean stock quotes, charts,
|
|
7
|
+
order books, indices, rankings, and investor trends in natural language, along
|
|
8
|
+
with your account's balance, holdings, and transaction history. For an ISA
|
|
9
|
+
account it also computes your realized-gain aggregation against the tax-free
|
|
10
|
+
allowance.
|
|
11
|
+
|
|
12
|
+
> ⚠️ **Security notice**
|
|
13
|
+
>
|
|
14
|
+
> - Run this server **locally only** — never expose it to an external network.
|
|
15
|
+
> - The AppKey/AppSecret in `.env` grant real-account inquiry access. **Never
|
|
16
|
+
> commit them** (`.env` is already in `.gitignore`).
|
|
17
|
+
> - Order execution (buy/sell/modify/cancel) is **excluded by design**. This
|
|
18
|
+
> server never changes your account.
|
|
19
|
+
|
|
20
|
+
## Tools
|
|
21
|
+
|
|
22
|
+
**Market data** (account-independent — an app key is enough):
|
|
23
|
+
|
|
24
|
+
| Tool | Description | Kiwoom TR |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| `search_stock` | Name → code search (KOSPI/KOSDAQ, incl. ETF/ETN) | ka10099 |
|
|
27
|
+
| `get_stock_price` | Current price / change rate / volume / basic metrics | ka10001 |
|
|
28
|
+
| `get_stock_chart` | Daily/weekly/monthly/minute candles (adjusted price) | ka10080~83 |
|
|
29
|
+
| `get_orderbook` | 10-level ask/bid quotes and sizes | ka10004 |
|
|
30
|
+
| `get_market_index` | KOSPI/KOSDAQ composite and sector indices | ka20003 |
|
|
31
|
+
| `get_ranking` | Top gainers / losers / volume / trading value | ka10027/30/32 |
|
|
32
|
+
| `get_investor_trend` | Retail / foreign / institutional net-buy trend (period sum + daily) | ka10059, ka10061 |
|
|
33
|
+
| `get_etf_info` | ETF tracking index / tax type / quote | ka40002, ka10001 |
|
|
34
|
+
| `get_short_selling` | Per-stock daily short-selling trend (short volume / weight / avg price) | ka10014 |
|
|
35
|
+
| `get_foreign_holding` | Per-stock foreign holding trend (holdings / holding weight / limit-usage rate) | ka10008 |
|
|
36
|
+
|
|
37
|
+
**Watchlist** (interest-stock groups saved in the 영웅문 HTS client — read-only):
|
|
38
|
+
|
|
39
|
+
| Tool | Description | Kiwoom TR |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `get_watchlist_groups` | List of saved watchlist groups (code + name) | ka01300 |
|
|
42
|
+
| `get_watchlist` | Stocks in a group (enriched with name / prev close / market) | ka01301, ka10099 |
|
|
43
|
+
|
|
44
|
+
> The Kiwoom REST API exposes no watchlist **edit** (add/remove) TR — read-only.
|
|
45
|
+
|
|
46
|
+
**Theme**:
|
|
47
|
+
|
|
48
|
+
| Tool | Description | Kiwoom TR |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `get_theme_groups` | Theme groups (change rate / stock count / period return / lead stocks; find a stock's themes) | ka90001 |
|
|
51
|
+
| `get_theme_stocks` | Member stocks of one theme with quotes (price / change / volume / period return) | ka90002 |
|
|
52
|
+
|
|
53
|
+
**Account** (scoped to the account bound to the app key):
|
|
54
|
+
|
|
55
|
+
| Tool | Description | Kiwoom TR |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `get_account_balance` | Deposit + total valuation / total P&L / estimated deposit assets | kt00001, kt00018 |
|
|
58
|
+
| `get_account_holdings` | Per-holding quantity / average cost / current price / valuation P&L | kt00018 |
|
|
59
|
+
| `get_transactions` | Transaction history for a period (trade date, unit price, settlement amount) | kt00015 |
|
|
60
|
+
| `get_pending_orders` | Open/unfilled orders (order no., side, status, ordered/unfilled qty, price) | ka10075 |
|
|
61
|
+
| `get_trading_journal` | The day's trading journal (per-stock buy/sell avg price, qty, realized P&L, totals) | ka10170 |
|
|
62
|
+
| `calc_isa_tax_status` | ISA aggregated net gain vs. the tax-free allowance (realized + full-liquidation scenario) | kt00015, ka10074, kt00018 |
|
|
63
|
+
|
|
64
|
+
Plus `ping` (connectivity check, no app key needed). Every response is prefixed
|
|
65
|
+
with `[모의투자]` (VIRTUAL) / `[실전투자]` (REAL) so you can tell which server
|
|
66
|
+
answered. The first `search_stock` call downloads the stock master (~4,300
|
|
67
|
+
symbols), takes a few seconds, and is then cached for 12 hours.
|
|
68
|
+
|
|
69
|
+
### Notes on `calc_isa_tax_status`
|
|
70
|
+
|
|
71
|
+
- Aggregation start date: `ISA_OPENED_ON` (account opening date) in `.env` is the
|
|
72
|
+
default; override per call with `from_date`.
|
|
73
|
+
- If dividends/distributions are not auto-detected from the transaction history,
|
|
74
|
+
enter them manually via the `dividends_received` argument.
|
|
75
|
+
- A stock's tax type (taxable vs. domestic-equity) is classified automatically —
|
|
76
|
+
for ETFs from Kiwoom's own `etftxon_type` (ka40002), otherwise by a name-based
|
|
77
|
+
heuristic. Correct a wrong classification with
|
|
78
|
+
`overrides: [{stock_code, tax_type}]`. Results are for reference only — actual
|
|
79
|
+
taxation follows the broker's settlement.
|
|
80
|
+
|
|
81
|
+
## Requirements
|
|
82
|
+
|
|
83
|
+
- **Node.js 20.12 or later** (uses `process.loadEnvFile`)
|
|
84
|
+
- A Kiwoom Securities REST API app key — register an app at the
|
|
85
|
+
[Kiwoom Open API portal](https://openapi.kiwoom.com) to obtain one.
|
|
86
|
+
- VIRTUAL (paper trading) and REAL use **separately issued app keys**; the key
|
|
87
|
+
type must match `KIWOOM_MODE`.
|
|
88
|
+
- The account is bound to the app key, so no account number is required.
|
|
89
|
+
|
|
90
|
+
## Install
|
|
91
|
+
|
|
92
|
+
The package is published to npm, so you can **run it with `npx` — no clone
|
|
93
|
+
required**; just use the npx config in "Connecting Claude Desktop" / "Connecting
|
|
94
|
+
Claude Code" below. In that case you pass your app key via the **client config's
|
|
95
|
+
`env` block** instead of a `.env` file (examples in each connection section).
|
|
96
|
+
|
|
97
|
+
To build from source or modify the code:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
git clone <this repo URL> # or copy the source
|
|
101
|
+
cd kiwoom-mcp-server
|
|
102
|
+
npm install
|
|
103
|
+
cp .env.example .env # fill in the values per the table below
|
|
104
|
+
npm run build # produces dist/
|
|
105
|
+
npm test # unit tests (no network required)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Environment variables (`.env`)
|
|
109
|
+
|
|
110
|
+
| Variable | Required | Description |
|
|
111
|
+
|---|---|---|
|
|
112
|
+
| `KIWOOM_APP_KEY` | ✅ | Kiwoom REST API app key |
|
|
113
|
+
| `KIWOOM_APP_SECRET` | ✅ | Kiwoom REST API app secret |
|
|
114
|
+
| `KIWOOM_MODE` | | `VIRTUAL` (paper trading, default) or `REAL` (live trading) |
|
|
115
|
+
| `ISA_TYPE` | | `GENERAL` (general, 2,000,000 KRW limit, default) or `SEOMIN` (low-income/farmer-fisher, 4,000,000 KRW) |
|
|
116
|
+
| `ISA_OPENED_ON` | | ISA account opening date `yyyy-MM-dd` — default aggregation start for `calc_isa_tax_status` |
|
|
117
|
+
|
|
118
|
+
If the account is not an ISA, leave `ISA_TYPE`/`ISA_OPENED_ON` blank — only
|
|
119
|
+
`calc_isa_tax_status` becomes unavailable; every other tool still works.
|
|
120
|
+
|
|
121
|
+
`.env` is resolved from the project root first, so the server works even when
|
|
122
|
+
launched from an arbitrary working directory (as Claude Desktop does).
|
|
123
|
+
|
|
124
|
+
## Connecting Claude Desktop
|
|
125
|
+
|
|
126
|
+
Config file location:
|
|
127
|
+
|
|
128
|
+
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
129
|
+
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
130
|
+
|
|
131
|
+
**npx (published) — no clone; pass your app key via the `env` block:**
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"mcpServers": {
|
|
136
|
+
"kiwoom": {
|
|
137
|
+
"command": "npx",
|
|
138
|
+
"args": ["-y", "kiwoom-mcp-server"],
|
|
139
|
+
"env": {
|
|
140
|
+
"KIWOOM_APP_KEY": "…",
|
|
141
|
+
"KIWOOM_APP_SECRET": "…",
|
|
142
|
+
"KIWOOM_MODE": "REAL"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**From source — run `dist/index.js` directly; app key from the project-root `.env`:**
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"mcpServers": {
|
|
154
|
+
"kiwoom": {
|
|
155
|
+
"command": "/opt/homebrew/bin/node",
|
|
156
|
+
"args": ["/absolute/path/kiwoom-mcp-server/dist/index.js"]
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
> **Prefer the absolute path to the executable** (find it with `which node` or
|
|
163
|
+
> `which npx`). GUI apps do not inherit your shell `PATH`, so a bare `"node"` /
|
|
164
|
+
> `"npx"` can make the server silently fail to start — the single most common
|
|
165
|
+
> failure.
|
|
166
|
+
|
|
167
|
+
After saving, fully quit Claude Desktop (⌘Q on macOS) and relaunch to see the
|
|
168
|
+
tools. **After every `npm run build` rebuild you must fully quit and relaunch
|
|
169
|
+
again** for the change to take effect.
|
|
170
|
+
|
|
171
|
+
## Connecting Claude Code
|
|
172
|
+
|
|
173
|
+
npx (published) — pass your app key via `-e` flags:
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
claude mcp add kiwoom \
|
|
177
|
+
-e KIWOOM_APP_KEY=… -e KIWOOM_APP_SECRET=… -e KIWOOM_MODE=REAL \
|
|
178
|
+
-- npx -y kiwoom-mcp-server
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
From source — app key from the project-root `.env`:
|
|
182
|
+
|
|
183
|
+
```sh
|
|
184
|
+
claude mcp add kiwoom -- node /absolute/path/kiwoom-mcp-server/dist/index.js
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Smoke test
|
|
188
|
+
|
|
189
|
+
You can verify over stdio without an MCP client:
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
printf '%s\n' \
|
|
193
|
+
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}' \
|
|
194
|
+
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
|
|
195
|
+
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ping","arguments":{}}}' \
|
|
196
|
+
| node dist/index.js
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`ping` responds without a `.env`. Market-data and account tools need an app key.
|
|
200
|
+
|
|
201
|
+
## Troubleshooting
|
|
202
|
+
|
|
203
|
+
| Symptom | Check |
|
|
204
|
+
|---|---|
|
|
205
|
+
| Tools don't appear in Desktop | Is `command` an **absolute** node path? Did you run `npm run build`? Did you fully quit and relaunch Desktop? |
|
|
206
|
+
| `환경설정이 없거나 잘못되었습니다` (config missing/invalid) | Is `.env` in the project root, with the required variables filled in? |
|
|
207
|
+
| `키움 인증에 실패했습니다` (auth failed) | Does the app-key type (paper/live) match `KIWOOM_MODE`? Is the app active on the portal? |
|
|
208
|
+
| `요청 한도를 초과했습니다` (rate limit) | Kiwoom rate limit (~1 req/s per TR). This means it was still exceeded after the server's automatic retry — wait a moment and retry. |
|
|
209
|
+
| Deposit differs from the D+2 estimated deposit | Normal when there are unsettled (D+2) trades. |
|
|
210
|
+
|
|
211
|
+
## Development
|
|
212
|
+
|
|
213
|
+
```sh
|
|
214
|
+
npm run dev # run the source directly with tsx
|
|
215
|
+
npm run typecheck # tsc --noEmit
|
|
216
|
+
npm test # vitest
|
|
217
|
+
npm run build # tsc → dist/
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Structure: `src/kiwoom/` (auth / HTTP / TR layer) → `src/tools/` (MCP tools with
|
|
221
|
+
formatters split out) → `src/isa/` (tax-type classification / realized-P&L
|
|
222
|
+
reconstruction / gain aggregation). See `CLAUDE.md` for detailed rules and the
|
|
223
|
+
verified API contract.
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
[MIT](LICENSE)
|
package/README.md
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# kiwoom-mcp-server
|
|
2
|
+
|
|
3
|
+
**한국어** · [English](README.en.md)
|
|
4
|
+
|
|
5
|
+
키움증권 REST API를 **조회 전용(read-only)** 으로 노출하는 MCP 서버입니다.
|
|
6
|
+
Claude Desktop / Claude Code에서 자연어로 국내 주식 시세·차트·호가·지수·순위·수급과
|
|
7
|
+
내 계좌의 잔고·보유 종목·거래내역을 조회할 수 있고, ISA 계좌라면 비과세 한도
|
|
8
|
+
대비 손익통산 현황까지 계산해 줍니다.
|
|
9
|
+
|
|
10
|
+
> ⚠️ **보안 경고**
|
|
11
|
+
>
|
|
12
|
+
> - 이 서버는 **로컬에서만 실행**하고 절대 외부 네트워크에 노출하지 마세요.
|
|
13
|
+
> - `.env`의 AppKey/AppSecret은 실계좌 조회 권한입니다. 절대 커밋하지 마세요
|
|
14
|
+
> (`.gitignore`에 이미 등록되어 있습니다).
|
|
15
|
+
> - 주문(매수/매도/정정/취소) 기능은 설계상 제외되어 있습니다. 이 서버가 계좌를
|
|
16
|
+
> 변경하는 일은 없습니다.
|
|
17
|
+
|
|
18
|
+
## 제공 Tool
|
|
19
|
+
|
|
20
|
+
**시장 데이터** (계좌와 무관, 앱키만 있으면 사용 가능):
|
|
21
|
+
|
|
22
|
+
| Tool | 설명 | 키움 TR |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `search_stock` | 종목명→코드 검색 (코스피/코스닥, ETF/ETN 포함) | ka10099 |
|
|
25
|
+
| `get_stock_price` | 현재가/등락률/거래량/기본지표 | ka10001 |
|
|
26
|
+
| `get_stock_chart` | 일/주/월/분봉 캔들 차트 (수정주가 반영) | ka10080~83 |
|
|
27
|
+
| `get_orderbook` | 10단계 매도/매수 호가·잔량 | ka10004 |
|
|
28
|
+
| `get_market_index` | 코스피/코스닥 종합·업종 지수 | ka20003 |
|
|
29
|
+
| `get_ranking` | 상승률/하락률/거래량/거래대금 상위 | ka10027/30/32 |
|
|
30
|
+
| `get_investor_trend` | 개인/외국인/기관 순매수 동향 (기간 합계 + 일별) | ka10059, ka10061 |
|
|
31
|
+
| `get_etf_info` | ETF 추적지수·과세유형·시세 | ka40002, ka10001 |
|
|
32
|
+
| `get_short_selling` | 종목별 일자별 공매도 추이 (공매도량·비중·평균가) | ka10014 |
|
|
33
|
+
| `get_foreign_holding` | 종목별 외국인 보유 추이 (보유주식수·보유비중·한도소진률) | ka10008 |
|
|
34
|
+
|
|
35
|
+
**관심종목** (영웅문 HTS에 저장한 관심 그룹, 읽기 전용):
|
|
36
|
+
|
|
37
|
+
| Tool | 설명 | 키움 TR |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `get_watchlist_groups` | HTS 관심종목 그룹 목록 (그룹코드+그룹명) | ka01300 |
|
|
40
|
+
| `get_watchlist` | 그룹 내 종목 목록 (종목명·전일종가·시장 보강) | ka01301, ka10099 |
|
|
41
|
+
|
|
42
|
+
> 키움 REST API에는 관심종목 **편집(추가/삭제)** TR이 없어 조회만 가능합니다.
|
|
43
|
+
|
|
44
|
+
**테마**:
|
|
45
|
+
|
|
46
|
+
| Tool | 설명 | 키움 TR |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `get_theme_groups` | 테마 그룹 목록 (등락률·종목수·기간수익률·주요종목; 종목별 편입 테마 검색) | ka90001 |
|
|
49
|
+
| `get_theme_stocks` | 특정 테마의 구성종목과 시세 (현재가·등락률·거래량·기간수익률) | ka90002 |
|
|
50
|
+
|
|
51
|
+
**계좌** (앱키에 귀속된 계좌 기준):
|
|
52
|
+
|
|
53
|
+
| Tool | 설명 | 키움 TR |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `get_account_balance` | 예수금 + 총평가금액/총평가손익/추정예탁자산 | kt00001, kt00018 |
|
|
56
|
+
| `get_account_holdings` | 보유 종목별 수량/평균단가/현재가/평가손익 | kt00018 |
|
|
57
|
+
| `get_transactions` | 기간별 거래내역 (체결일·단가·정산금액) | kt00015 |
|
|
58
|
+
| `get_pending_orders` | 미체결 주문 (주문번호·구분·상태·주문/미체결수량·주문가격) | ka10075 |
|
|
59
|
+
| `get_trading_journal` | 당일매매일지 (종목별 매수/매도 평균가·수량·실현손익, 총손익) | ka10170 |
|
|
60
|
+
| `calc_isa_tax_status` | ISA 손익통산 순이익의 비과세 한도 대비 현황 (확정 + 전량매도 시나리오) | kt00015, ka10074, kt00018 |
|
|
61
|
+
|
|
62
|
+
그 외 `ping`(연결 확인, 앱키 불필요). 모든 응답은 `[모의투자]`/`[실전투자]` 접두어로
|
|
63
|
+
어느 서버가 답했는지 표시합니다. `search_stock` 첫 호출은 종목 마스터(~4,300종목)를
|
|
64
|
+
내려받아 몇 초 걸리며 이후 12시간 캐시됩니다.
|
|
65
|
+
|
|
66
|
+
### `calc_isa_tax_status` 사용 메모
|
|
67
|
+
|
|
68
|
+
- 집계 시작일: `.env`의 `ISA_OPENED_ON`(계좌 개설일)이 기본값, 호출 시 `from_date`로 오버라이드 가능.
|
|
69
|
+
- 배당·분배금이 거래내역에서 자동 감지되지 않으면 `dividends_received` 인자로 수동 입력.
|
|
70
|
+
- 종목 과세유형(과세대상 vs 국내주식형)은 종목명 기반 자동 분류이며, 틀린 경우
|
|
71
|
+
`overrides: [{stock_code, tax_type}]`로 수정. 결과는 참고용 — 실제 과세는 증권사 정산 기준.
|
|
72
|
+
|
|
73
|
+
## 요구 사항
|
|
74
|
+
|
|
75
|
+
- **Node.js 20.12 이상** (`process.loadEnvFile` 사용)
|
|
76
|
+
- 키움증권 REST API 앱키 — [키움 Open API 포털](https://openapi.kiwoom.com)에서
|
|
77
|
+
앱 등록 후 발급
|
|
78
|
+
- 모의투자(VIRTUAL)와 실전투자(REAL)는 **각각 별도로 발급받은 앱키**를 사용하며,
|
|
79
|
+
발급받은 키 종류와 `KIWOOM_MODE`가 일치해야 합니다.
|
|
80
|
+
- 계좌는 앱키에 귀속되므로 계좌번호 입력은 필요 없습니다.
|
|
81
|
+
|
|
82
|
+
## 설치
|
|
83
|
+
|
|
84
|
+
npm에 배포되어 있으므로 **클론 없이 `npx`로 바로 실행**할 수 있습니다 — 아래
|
|
85
|
+
"Claude Desktop 연결" / "Claude Code 연결"의 npx 설정을 그대로 쓰면 됩니다. 이 경우
|
|
86
|
+
앱키는 `.env` 파일 대신 **클라이언트 설정의 `env` 블록**으로 전달합니다(예제는 각
|
|
87
|
+
연결 섹션 참고).
|
|
88
|
+
|
|
89
|
+
소스에서 직접 빌드하거나 코드를 수정하려면:
|
|
90
|
+
|
|
91
|
+
```sh
|
|
92
|
+
git clone <이 저장소 URL> # 또는 소스 복사
|
|
93
|
+
cd kiwoom-mcp-server
|
|
94
|
+
npm install
|
|
95
|
+
cp .env.example .env # 아래 표를 참고해 값 입력
|
|
96
|
+
npm run build # dist/ 생성
|
|
97
|
+
npm test # 단위 테스트 (네트워크 불필요)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## 환경 변수 (`.env`)
|
|
101
|
+
|
|
102
|
+
| 변수 | 필수 | 설명 |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `KIWOOM_APP_KEY` | ✅ | 키움 REST API 앱키 |
|
|
105
|
+
| `KIWOOM_APP_SECRET` | ✅ | 키움 REST API 앱 시크릿 |
|
|
106
|
+
| `KIWOOM_MODE` | | `VIRTUAL`(모의투자, 기본값) 또는 `REAL`(실전투자) |
|
|
107
|
+
| `ISA_TYPE` | | `GENERAL`(일반형, 한도 200만원, 기본값) 또는 `SEOMIN`(서민형/농어민형, 400만원) |
|
|
108
|
+
| `ISA_OPENED_ON` | | ISA 계좌 개설일 `yyyy-MM-dd` — `calc_isa_tax_status` 집계 시작일 기본값 |
|
|
109
|
+
|
|
110
|
+
ISA 계좌가 아니라면 `ISA_TYPE`/`ISA_OPENED_ON`은 비워 두면 됩니다 —
|
|
111
|
+
`calc_isa_tax_status`만 사용할 수 없고 나머지 tool은 모두 동작합니다.
|
|
112
|
+
|
|
113
|
+
`.env`는 프로젝트 루트에서 먼저 찾기 때문에 (Claude Desktop처럼) 임의의 작업
|
|
114
|
+
디렉터리에서 실행돼도 동작합니다.
|
|
115
|
+
|
|
116
|
+
## Claude Desktop 연결
|
|
117
|
+
|
|
118
|
+
설정 파일 위치:
|
|
119
|
+
|
|
120
|
+
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
121
|
+
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
122
|
+
|
|
123
|
+
**npx (배포판) — 클론 없이 실행. 앱키는 `env` 블록으로 전달:**
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"mcpServers": {
|
|
128
|
+
"kiwoom": {
|
|
129
|
+
"command": "npx",
|
|
130
|
+
"args": ["-y", "kiwoom-mcp-server"],
|
|
131
|
+
"env": {
|
|
132
|
+
"KIWOOM_APP_KEY": "…",
|
|
133
|
+
"KIWOOM_APP_SECRET": "…",
|
|
134
|
+
"KIWOOM_MODE": "REAL"
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**소스 빌드 — `dist/index.js` 직접 실행. 앱키는 프로젝트 루트 `.env` 사용:**
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"mcpServers": {
|
|
146
|
+
"kiwoom": {
|
|
147
|
+
"command": "/opt/homebrew/bin/node",
|
|
148
|
+
"args": ["/절대/경로/kiwoom-mcp-server/dist/index.js"]
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
> **`command`에는 실행 파일의 절대 경로**를 쓰는 편이 가장 안전합니다 (`which node`,
|
|
155
|
+
> `which npx`로 확인). GUI 앱은 셸 PATH를 상속받지 않으므로 `"node"`/`"npx"`라고만
|
|
156
|
+
> 쓰면 서버가 조용히 뜨지 않을 수 있습니다 — 가장 흔한 실패 원인입니다.
|
|
157
|
+
|
|
158
|
+
저장 후 Claude Desktop을 완전히 종료(macOS는 ⌘Q)했다가 다시 실행하면 tool이
|
|
159
|
+
보입니다. `npm run build`로 **다시 빌드한 뒤에도 완전 종료 후 재실행**해야
|
|
160
|
+
변경이 반영됩니다.
|
|
161
|
+
|
|
162
|
+
## Claude Code 연결
|
|
163
|
+
|
|
164
|
+
npx (배포판) — 앱키는 `-e` 플래그로 전달:
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
claude mcp add kiwoom \
|
|
168
|
+
-e KIWOOM_APP_KEY=… -e KIWOOM_APP_SECRET=… -e KIWOOM_MODE=REAL \
|
|
169
|
+
-- npx -y kiwoom-mcp-server
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
소스 빌드 — 프로젝트 루트 `.env` 사용:
|
|
173
|
+
|
|
174
|
+
```sh
|
|
175
|
+
claude mcp add kiwoom -- node /절대/경로/kiwoom-mcp-server/dist/index.js
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## 동작 확인
|
|
179
|
+
|
|
180
|
+
MCP 클라이언트 없이 stdio로 직접 확인할 수 있습니다:
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
printf '%s\n' \
|
|
184
|
+
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}' \
|
|
185
|
+
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
|
|
186
|
+
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ping","arguments":{}}}' \
|
|
187
|
+
| node dist/index.js
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
`ping`은 `.env` 없이도 응답합니다. 시세·계좌 tool은 앱키가 있어야 합니다.
|
|
191
|
+
|
|
192
|
+
## 문제 해결
|
|
193
|
+
|
|
194
|
+
| 증상 | 확인할 것 |
|
|
195
|
+
|---|---|
|
|
196
|
+
| Desktop에 tool이 안 보임 | `command`가 node **절대 경로**인지, `npm run build`를 했는지, Desktop을 완전 종료 후 재실행했는지 |
|
|
197
|
+
| `환경설정이 없거나 잘못되었습니다` | `.env`가 프로젝트 루트에 있는지, 필수 변수가 채워졌는지 |
|
|
198
|
+
| `키움 인증에 실패했습니다` | 앱키 종류(모의/실전)와 `KIWOOM_MODE`가 일치하는지, 포털에서 앱이 활성 상태인지 |
|
|
199
|
+
| `요청 한도를 초과했습니다` | 키움 레이트리밋(TR당 약 1초 1회). 서버가 자동 재시도한 뒤에도 초과한 경우이니 잠시 후 다시 시도 |
|
|
200
|
+
| 예수금과 D+2 추정예수금이 다름 | 미결제(D+2 정산) 매매가 있으면 정상입니다 |
|
|
201
|
+
|
|
202
|
+
## 개발
|
|
203
|
+
|
|
204
|
+
```sh
|
|
205
|
+
npm run dev # tsx로 소스 직접 실행
|
|
206
|
+
npm run typecheck # tsc --noEmit
|
|
207
|
+
npm test # vitest
|
|
208
|
+
npm run build # tsc → dist/
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
구조: `src/kiwoom/`(인증·HTTP·TR 계층) → `src/tools/`(MCP tool, 포맷터 분리) →
|
|
212
|
+
`src/isa/`(과세유형 분류·실현손익 재구성·손익통산). 상세 규칙과 검증된 API
|
|
213
|
+
계약은 `CLAUDE.md` 참고.
|
|
214
|
+
|
|
215
|
+
## 라이선스
|
|
216
|
+
|
|
217
|
+
[MIT](LICENSE)
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
const envSchema = z.object({
|
|
5
|
+
KIWOOM_APP_KEY: z.string().min(1),
|
|
6
|
+
KIWOOM_APP_SECRET: z.string().min(1),
|
|
7
|
+
KIWOOM_MODE: z.enum(["VIRTUAL", "REAL"]).default("VIRTUAL"),
|
|
8
|
+
ISA_TYPE: z.enum(["GENERAL", "SEOMIN"]).default("GENERAL"),
|
|
9
|
+
/** ISA account opening date — default aggregation start for tax-status math. */
|
|
10
|
+
ISA_OPENED_ON: z
|
|
11
|
+
.string()
|
|
12
|
+
.regex(/^\d{4}-?\d{2}-?\d{2}$/)
|
|
13
|
+
.optional(),
|
|
14
|
+
});
|
|
15
|
+
const BASE_URLS = {
|
|
16
|
+
VIRTUAL: "https://mockapi.kiwoom.com",
|
|
17
|
+
REAL: "https://api.kiwoom.com",
|
|
18
|
+
};
|
|
19
|
+
const MODE_LABELS = {
|
|
20
|
+
VIRTUAL: "모의투자",
|
|
21
|
+
REAL: "실전투자",
|
|
22
|
+
};
|
|
23
|
+
let cached = null;
|
|
24
|
+
function loadDotEnv() {
|
|
25
|
+
// Claude Desktop launches this server with an arbitrary cwd, so resolve .env
|
|
26
|
+
// relative to the project root (parent of dist/ or src/) first.
|
|
27
|
+
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
28
|
+
for (const candidate of [path.join(projectRoot, ".env"), path.join(process.cwd(), ".env")]) {
|
|
29
|
+
try {
|
|
30
|
+
process.loadEnvFile(candidate);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// File missing — try the next candidate; env vars may also be set directly.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function getConfig() {
|
|
39
|
+
if (cached)
|
|
40
|
+
return cached;
|
|
41
|
+
loadDotEnv();
|
|
42
|
+
const parsed = envSchema.safeParse(process.env);
|
|
43
|
+
if (!parsed.success) {
|
|
44
|
+
const problems = parsed.error.issues
|
|
45
|
+
.map((issue) => issue.path.join("."))
|
|
46
|
+
.join(", ");
|
|
47
|
+
throw new Error(`환경설정이 없거나 잘못되었습니다: ${problems}. ` +
|
|
48
|
+
`프로젝트 루트의 .env.example을 .env로 복사한 뒤 키움 REST API 앱키를 입력해 주세요.`);
|
|
49
|
+
}
|
|
50
|
+
const env = parsed.data;
|
|
51
|
+
cached = {
|
|
52
|
+
appKey: env.KIWOOM_APP_KEY,
|
|
53
|
+
appSecret: env.KIWOOM_APP_SECRET,
|
|
54
|
+
mode: env.KIWOOM_MODE,
|
|
55
|
+
modeLabel: MODE_LABELS[env.KIWOOM_MODE],
|
|
56
|
+
isaType: env.ISA_TYPE,
|
|
57
|
+
isaOpenedOn: env.ISA_OPENED_ON?.replaceAll("-", ""),
|
|
58
|
+
baseUrl: BASE_URLS[env.KIWOOM_MODE],
|
|
59
|
+
};
|
|
60
|
+
return cached;
|
|
61
|
+
}
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getConfig } from "./config.js";
|
|
2
|
+
import { TokenManager } from "./kiwoom/auth.js";
|
|
3
|
+
import { KiwoomClient } from "./kiwoom/client.js";
|
|
4
|
+
let context = null;
|
|
5
|
+
/**
|
|
6
|
+
* Lazily builds the shared Kiwoom client. Config errors (missing .env) are
|
|
7
|
+
* thrown here — at tool-call time — so the server still starts and `ping`
|
|
8
|
+
* works without credentials.
|
|
9
|
+
*/
|
|
10
|
+
export function getKiwoomContext() {
|
|
11
|
+
if (!context) {
|
|
12
|
+
const config = getConfig();
|
|
13
|
+
context = { config, client: new KiwoomClient(config, new TokenManager(config)) };
|
|
14
|
+
}
|
|
15
|
+
return context;
|
|
16
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { createServer, SERVER_NAME, SERVER_VERSION } from "./server.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
const server = createServer();
|
|
6
|
+
const transport = new StdioServerTransport();
|
|
7
|
+
await server.connect(transport);
|
|
8
|
+
// stdout is reserved for MCP protocol frames — every log must go to stderr,
|
|
9
|
+
// or Claude Desktop/Code will fail to parse the stream.
|
|
10
|
+
console.error(`${SERVER_NAME} v${SERVER_VERSION} running on stdio`);
|
|
11
|
+
}
|
|
12
|
+
main().catch((error) => {
|
|
13
|
+
console.error("Fatal error while starting kiwoom-mcp-server:", error);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ka40002(ETF종목정보)의 `etftxon_type`을 사용한 권위 있는 과세유형 분류.
|
|
3
|
+
*
|
|
4
|
+
* ETF는 키움이 직접 제공하는 과세유형으로 확정하고, 개별주식·브랜드 미매칭·API
|
|
5
|
+
* 실패 시에는 `classifyInstrument`의 종목명 휴리스틱으로 폴백한다. 수동 지정
|
|
6
|
+
* (`overrides`)은 항상 최우선이며 이 경우 API를 호출하지 않는다.
|
|
7
|
+
*
|
|
8
|
+
* ka40002는 동일 TR 기준 ~1 req/s 제한이 있으므로 ETF 코드만 순차 호출하고,
|
|
9
|
+
* 과세유형은 사실상 불변이라 프로세스 내에서 길게 캐시한다.
|
|
10
|
+
*/
|
|
11
|
+
import { fetchEtfInfo } from "../kiwoom/api.js";
|
|
12
|
+
import { sleep } from "../utils/sleep.js";
|
|
13
|
+
import { classifyInstrument, isLikelyEtf, mapEtfTaxonType, } from "./classify.js";
|
|
14
|
+
/** 과세유형은 상장 폐지 전까지 바뀌지 않으므로 길게 캐시해도 안전하다. */
|
|
15
|
+
const CACHE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
16
|
+
/** ka40002는 동일 TR 1 req/s — 캐시 미스로 연속 호출할 때만 간격을 둔다. */
|
|
17
|
+
const ETF_FETCH_GAP_MS = 1_100;
|
|
18
|
+
let cache = new Map();
|
|
19
|
+
/** 테스트 훅 — 모듈 레벨 ka40002 과세유형 캐시를 비운다. */
|
|
20
|
+
export function clearEtfTaxonCache() {
|
|
21
|
+
cache = new Map();
|
|
22
|
+
}
|
|
23
|
+
function freshEntry(code) {
|
|
24
|
+
const hit = cache.get(code);
|
|
25
|
+
if (hit && Date.now() - hit.fetchedAt < CACHE_TTL_MS)
|
|
26
|
+
return hit;
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
/** ka40002 1회 호출; 성공(빈 값 포함)은 캐시하고, 실패는 캐시하지 않는다. */
|
|
30
|
+
async function fetchTaxonType(client, code) {
|
|
31
|
+
try {
|
|
32
|
+
const etf = await fetchEtfInfo(client, code);
|
|
33
|
+
// stk_nm이 비면 ETF가 아님(비-ETF 코드) → null로 확정 캐시.
|
|
34
|
+
const taxonType = etf.stk_nm ? etf.etftxon_type.trim() || null : null;
|
|
35
|
+
cache.set(code, { fetchedAt: Date.now(), taxonType });
|
|
36
|
+
return taxonType;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null; // API 실패 → 휴리스틱 폴백 (다음 호출에서 재시도되도록 캐시하지 않음)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 종목 목록을 코드 기준으로 dedup 하여 과세유형을 분류한다. ETF는 ka40002로
|
|
44
|
+
* 확정하고 나머지는 휴리스틱으로 폴백한 `Map<code, Classification>`을 반환한다.
|
|
45
|
+
*
|
|
46
|
+
* @param etfCodes ka10099 마스터리스트에서 `marketName == "ETF"`로 확인된 코드 집합.
|
|
47
|
+
* 브랜드 접두어가 없어 이름만으로는 ETF로 안 보이는 종목도 여기 있으면 ka40002
|
|
48
|
+
* 확정 분류 대상에 포함한다(전달하지 않으면 종목명 브랜드 휴리스틱만 사용).
|
|
49
|
+
*/
|
|
50
|
+
export async function classifyInstruments(client, instruments, overrides, etfCodes) {
|
|
51
|
+
const unique = new Map();
|
|
52
|
+
for (const { code, name } of instruments) {
|
|
53
|
+
if (!unique.has(code))
|
|
54
|
+
unique.set(code, name);
|
|
55
|
+
}
|
|
56
|
+
const result = new Map();
|
|
57
|
+
let priorFetch = false;
|
|
58
|
+
for (const [code, name] of unique) {
|
|
59
|
+
// 브랜드 접두어(이름) 또는 마스터리스트 ETF 표시 중 하나라도 맞으면 ETF로 취급.
|
|
60
|
+
const isEtf = isLikelyEtf(name) || (etfCodes?.has(code) ?? false);
|
|
61
|
+
const heuristic = classifyInstrument(code, name, overrides, isEtf);
|
|
62
|
+
// 수동 지정은 최우선, ETF가 아닌 종목은 조회할 것이 없다.
|
|
63
|
+
if (overrides?.get(code) || !isEtf) {
|
|
64
|
+
result.set(code, heuristic);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const cached = freshEntry(code);
|
|
68
|
+
let taxonType;
|
|
69
|
+
if (cached) {
|
|
70
|
+
taxonType = cached.taxonType;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
if (priorFetch)
|
|
74
|
+
await sleep(ETF_FETCH_GAP_MS);
|
|
75
|
+
priorFetch = true;
|
|
76
|
+
taxonType = await fetchTaxonType(client, code);
|
|
77
|
+
}
|
|
78
|
+
const mapped = taxonType ? mapEtfTaxonType(taxonType) : null;
|
|
79
|
+
result.set(code, mapped
|
|
80
|
+
? { taxType: mapped, confident: true, reason: `키움 과세유형: ${taxonType.trim()}` }
|
|
81
|
+
: heuristic);
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|