tossinvest-mcp 0.3.0 → 0.3.2

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## 프로그램 개요
4
4
 
5
- 현재 버전: **0.3.0**
5
+ 현재 버전: **0.3.2**
6
6
 
7
7
  `tossinvest-mcp`는 토스증권 Open API(현재가 조회, 주문, 잔고 조회 등)를 OpenAI 서비스, Claude Desktop, Cursor 같은 MCP 지원 프로그램에서 사용할 수 있게 해주는 로컬 MCP 서버입니다.
8
8
 
@@ -35,50 +35,57 @@
35
35
 
36
36
  거래 기능을 켜지 않아도 아래 기능을 사용할 수 있습니다.
37
37
 
38
- - 국내/미국 주식 현재가 조회
39
- - 호가 조회
40
- - 최근 체결 내역 조회
41
- - 캔들 차트 조회
42
- - 상한가/하한가 조회
43
- - 종목 기본 정보 조회
44
- - 전체 종목 목록 조회
45
- - 매수 유의사항 조회
46
- - 종목별 투자자·프로그램·공매도·신용거래·대차거래 동향 조회
47
- - 주식 랭킹 조회
48
- - 시장 지표 현재가·차트·투자자 매매동향 조회
49
- - 섹터 목록·상세·랭킹·소속 주식·ETF 조회
50
- - 환율 조회
51
- - 국내/미국 장 운영 시간 조회
52
- - 계좌 목록 조회
53
- - 보유 주식 조회
54
- - 매수 가능 금액 조회
55
- - 판매 가능 수량 조회
56
- - 매매 수수료 조회
57
- - 주문 목록 조회
58
- - 주문 상세 조회
59
- - 조건주문 목록·상세 조회
38
+ | 도구명 | 기능 |
39
+ | --- | --- |
40
+ | `toss_get_prices` | 국내·미국 주식 현재가 조회 |
41
+ | `toss_get_orderbook` | 매수·매도 호가 조회 |
42
+ | `toss_get_trades` | 종목의 최근 체결 내역 조회 |
43
+ | `toss_get_candles` | 종목의 분봉·일봉 차트 조회 |
44
+ | `toss_get_price_limits` | 종목의 상한가·하한가 조회 |
45
+ | `toss_get_stocks` | 종목명·시장·통화 등 기본 정보 조회 |
46
+ | `toss_list_stocks` | 거래소별 전체 종목 목록 조회. 심볼만 필요하면 `symbolsOnly: true`로 전체 심볼 목록을 한 번에 조회 |
47
+ | `toss_get_stock_warnings` | 종목의 매수 유의사항 조회 |
48
+ | `toss_get_stock_investor_trading` | 국내 종목의 투자자별 매매동향 조회 |
49
+ | `toss_get_stock_program_trades` | 국내 종목의 프로그램매매 동향 조회 |
50
+ | `toss_get_stock_short_selling` | 국내 종목의 공매도 동향 조회 |
51
+ | `toss_get_stock_credit_trades` | 국내 종목의 신용거래 동향 조회 |
52
+ | `toss_get_stock_securities_lending` | 국내 종목의 대차거래 동향 조회 |
53
+ | `toss_get_rankings` | 국내·미국 주식의 거래대금·거래량·등락률 등 랭킹 조회 |
54
+ | `toss_get_investor_trading_rankings` | 국내 종목의 외국인·기관·개인 순매수·순매도 랭킹 조회 |
55
+ | `toss_get_market_indicator_prices` | 국내 지수·국채 등 시장 지표의 현재가 조회 |
56
+ | `toss_get_market_indicator_candles` | 시장 지표의 캔들 차트 조회 |
57
+ | `toss_get_market_indicator_investor_trading` | 코스피·코스닥 시장의 투자자별 매매대금 조회 |
58
+ | `toss_list_sectors` | 섹터 목록 조회 |
59
+ | `toss_get_sector` | 섹터 상세 정보 조회 |
60
+ | `toss_get_sector_rankings` | 섹터별 등락률·거래대금 랭킹 조회 |
61
+ | `toss_get_sector_stocks` | 섹터에 포함된 주식 조회 |
62
+ | `toss_get_sector_etfs` | 섹터 관련 ETF 조회 |
63
+ | `toss_get_exchange_rate` | 원화·달러 환율 조회 |
64
+ | `toss_get_kr_market_calendar` | 국내 주식시장 운영일·시간 조회 |
65
+ | `toss_get_us_market_calendar` | 미국 주식시장 운영일·시간 조회 |
66
+ | `toss_get_accounts` | 사용 가능한 계좌 목록 조회 |
67
+ | `toss_get_holdings` | 계좌의 보유 주식 조회 |
68
+ | `toss_get_transactions` | 계좌의 매매·입출금·환전·배당 등 거래내역 조회 |
69
+ | `toss_get_buying_power` | 계좌의 매수 가능 금액 조회 |
70
+ | `toss_get_sellable_quantity` | 종목의 매도 가능 수량 조회 |
71
+ | `toss_get_commissions` | 계좌의 매매 수수료 조회 |
72
+ | `toss_get_orders` | 일반 주문 목록 조회 |
73
+ | `toss_get_order` | 일반 주문 상세 조회 |
74
+ | `toss_get_conditional_orders` | 조건주문 목록 조회 |
75
+ | `toss_get_conditional_order` | 조건주문 상세 조회 |
60
76
 
61
77
  거래 기능을 켠 경우 사용할 수 있는 도구:
62
78
 
63
- - `toss_create_order`: 주문 생성
64
- - `toss_modify_order`: 주문 정정
65
- - `toss_cancel_order`: 주문 취소
66
- - `toss_create_conditional_order`: 조건주문 생성
67
- - `toss_modify_conditional_order`: 조건주문 수정
68
- - `toss_cancel_conditional_order`: 조건주문 취소
69
-
70
- 0.3.0은 REST 도구 20개를 추가해 **조회 34개·거래 6개**를 제공합니다. 로컬 stdio 방식이며 WebSocket이나 원격 HTTP/SSE 서버는 제공하지 않습니다.
71
-
72
- 특징:
79
+ | 도구명 | 기능 |
80
+ | --- | --- |
81
+ | `toss_create_order` | 일반 주문 생성 |
82
+ | `toss_modify_order` | 일반 주문 정정 |
83
+ | `toss_cancel_order` | 일반 주문 취소 |
84
+ | `toss_create_conditional_order` | 조건주문 생성 |
85
+ | `toss_modify_conditional_order` | 조건주문 수정 |
86
+ | `toss_cancel_conditional_order` | 조건주문 취소 |
73
87
 
74
- - `npx`로 실행되므로 별도 프로그램 설치 과정이 짧습니다.
75
- - Client ID와 Client Secret은 환경변수로 전달합니다. 이전 이름인 API Key·Secret Key 환경변수도 호환됩니다.
76
- - OAuth 토큰은 내부에서 자동 발급하고, 만료 60초 전에 갱신합니다. 같은 컴퓨터·OS 사용자·저장 경로에서는 여러 프로세스가 토큰과 주문 집계를 공유합니다.
77
- - 계좌가 필요한 기능은 AI가 계좌 목록을 확인한 뒤 `accountSeq`를 입력합니다. 계좌 1을 자동 선택하지 않습니다.
78
- - `TOSSINVEST_ACCOUNT`를 설정하면 해당 계좌로 고정하며, 다른 계좌를 지정한 호출은 거절합니다.
79
- - 일반 주문·조건주문 생성, 정정, 취소 기능은 기본적으로 꺼져 있습니다.
80
- - 거래 기능을 켜면 `confirmOrderAction` 없이 주문할 수 있습니다. 매도·시장가 차단 설정도 제거되었습니다.
81
- - 1회 금액 한도, 시장별 일일 금액·건수 한도, 종목 허용목록은 필요한 항목만 환경변수로 설정할 수 있습니다. (아래 `거래 가드레일` 참고)
88
+ 0.3.1 이상 버전은 투자자별 순매수·순매도 랭킹과 계좌 거래내역 조회를 추가해 **조회 36개·거래 6개**를 제공합니다. 로컬 stdio 방식이며 WebSocket이나 원격 HTTP/SSE 서버는 제공하지 않습니다.
82
89
 
83
90
  ## 설치방법
84
91
 
@@ -143,7 +150,7 @@ MCP 클라이언트의 설정 파일 또는 MCP Servers 설정 화면에 아래
143
150
  "mcpServers": {
144
151
  "tossinvest": {
145
152
  "command": "npx",
146
- "args": ["-y", "tossinvest-mcp@0.3.0"],
153
+ "args": ["-y", "tossinvest-mcp"],
147
154
  "env": {
148
155
  "TOSSINVEST_CLIENT_ID": "발급받은-Client-ID",
149
156
  "TOSSINVEST_CLIENT_SECRET": "발급받은-Client-Secret"
@@ -155,7 +162,7 @@ MCP 클라이언트의 설정 파일 또는 MCP Servers 설정 화면에 아래
155
162
 
156
163
  설정 후 MCP 클라이언트를 완전히 종료했다가 다시 실행하세요.
157
164
 
158
- 처음 실행할 때 npm이 지정한 버전의 `tossinvest-mcp` 패키지를 내려받습니다. 위 예시는 0.3.0을 고정해 실행합니다.
165
+ 처음 실행할 때 npm이 지정한 버전의 `tossinvest-mcp` 패키지를 내려받습니다.
159
166
 
160
167
  `env` 안에 연결 정보를 넣으면 별도 `.env` 파일은 필요하지 않습니다. 서버가 `.env` 파일을 자동으로 읽지는 않습니다. 다른 MCP가 이미 등록되어 있다면 기존 설정을 덮어쓰지 말고 `mcpServers` 안에 `tossinvest` 항목만 추가하세요.
161
168
 
@@ -178,7 +185,7 @@ MCP 클라이언트에서 다음처럼 요청합니다.
178
185
  "mcpServers": {
179
186
  "tossinvest": {
180
187
  "command": "npx",
181
- "args": ["-y", "tossinvest-mcp@0.3.0"],
188
+ "args": ["-y", "tossinvest-mcp"],
182
189
  "env": {
183
190
  "TOSSINVEST_CLIENT_ID": "발급받은-Client-ID",
184
191
  "TOSSINVEST_CLIENT_SECRET": "발급받은-Client-Secret",
@@ -228,7 +235,7 @@ AAPL 최근 체결 내역 10개를 조회해줘.
228
235
  "mcpServers": {
229
236
  "tossinvest": {
230
237
  "command": "npx",
231
- "args": ["-y", "tossinvest-mcp@0.3.0"],
238
+ "args": ["-y", "tossinvest-mcp"],
232
239
  "env": {
233
240
  "TOSSINVEST_CLIENT_ID": "발급받은-Client-ID",
234
241
  "TOSSINVEST_CLIENT_SECRET": "발급받은-Client-Secret",
@@ -242,7 +249,7 @@ AAPL 최근 체결 내역 10개를 조회해줘.
242
249
 
243
250
  `TOSSINVEST_ACCOUNT`의 2는 예시입니다. 계좌 목록에서 확인한 순번으로 바꾸세요. 설정 후 MCP 클라이언트를 완전히 종료했다가 다시 실행합니다.
244
251
 
245
- 0.3.0에서는 **`confirmOrderAction` 확인값 없이 일반 주문과 조건주문을 생성·정정·취소할 수 있습니다.** 매도와 시장가도 거래 기능을 켜면 사용할 수 있습니다. 금액·건수 한도가 필요하다면 아래 설정을 함께 넣으세요.
252
+ 0.3.0 버전 이상에서는 **`confirmOrderAction` 확인값 없이 일반 주문과 조건주문을 생성·정정·취소할 수 있습니다.** 매도와 시장가도 거래 기능을 켜면 사용할 수 있습니다. 금액·건수 한도가 필요하다면 아래 설정을 함께 넣으세요.
246
253
 
247
254
  MCP 클라이언트에서 자연어로 요청할 때는 아래처럼 구체적으로 말하는 것이 좋습니다.
248
255
 
@@ -346,7 +353,7 @@ MCP 클라이언트에서 자연어로 요청할 때는 아래처럼 구체적
346
353
  "mcpServers": {
347
354
  "tossinvest": {
348
355
  "command": "npx",
349
- "args": ["-y", "tossinvest-mcp@0.3.0"],
356
+ "args": ["-y", "tossinvest-mcp"],
350
357
  "env": {
351
358
  "TOSSINVEST_CLIENT_ID": "발급받은-Client-ID",
352
359
  "TOSSINVEST_CLIENT_SECRET": "발급받은-Client-Secret",
@@ -407,7 +414,7 @@ Node.js가 설치되어 있지 않거나 PATH에 등록되지 않은 상태입
407
414
 
408
415
  ### 계좌 조회는 되는데 보유 주식 조회가 실패합니다
409
416
 
410
- 계좌 목록에서 `accountSeq`를 확인하고 AI에게 사용할 계좌를 알려주세요. 0.3.0은 계좌 1을 자동 선택하지 않습니다. `TOSSINVEST_ACCOUNT`로 계좌를 고정한 경우에는 다른 계좌를 입력할 수 없습니다.
417
+ 계좌 목록에서 `accountSeq`를 확인하고 AI에게 사용할 계좌를 알려주세요. 0.3.0버전 이상은 계좌 1을 자동 선택하지 않습니다. `TOSSINVEST_ACCOUNT`로 계좌를 고정한 경우에는 다른 계좌를 입력할 수 없습니다.
411
418
 
412
419
  ### 주문 도구가 보이지 않습니다
413
420
 
@@ -526,19 +533,10 @@ Node.js가 설치되어 있지 않거나 PATH에 등록되지 않은 상태입
526
533
  - 조건주문 취소의 정상 `204 No Content`를 지원합니다. 취소 후 상세가 404로 사라지면 예산을 환급하지 않고 유지하며 경고합니다. 상세 조회는 생성·정정을 재전송하지 않습니다.
527
534
  - SDK를 1.32.1로 올렸습니다. 개발용 UI·mock 서버·검증 스크립트·테스트 자료는 GitHub·npm 배포 대상에서 제외했고, CI는 타입 검사·빌드·패키징을 확인합니다.
528
535
 
529
- ### 기존 상태 파일 이전
530
-
531
- 업그레이드 전에 기존 MCP 프로세스를 모두 종료하세요. 이전 `guard-state.json`은 백업 후 현재·전일의 검증 가능한 기록을 중복 제거해 시장별 초기 사용액·건수로 한 번 가져옵니다. 이전 파일에는 API 키 식별자가 없고 미체결 단가를 완전히 복구할 수 없어, 과거 취소 환급은 추정하지 않습니다. 손상 파일을 조용히 0으로 초기화하지 않습니다.
532
-
533
- 개발 중인 0.3.0의 v3(자정 기준) 장부가 있으면 백업 후 v4로 보수적으로 이전합니다. 첫 집계 구간의 사용액이 실제보다 클 수 있습니다. 시장을 구분할 수 없는 과거 합산 건수는 각 시장 상한에 보수적으로 적용하고, 두 시장이 각각 다음 초기화 시각을 지날 때까지 유지합니다.
534
-
535
- 구버전으로 돌아갈 때는 백업 파일과 기존 설정을 사용해야 하며 새 버전에서 추가한 거래는 구버전 파일에 반영되지 않습니다. 구·신 버전을 같은 키로 동시에 실행하지 마세요.
536
-
537
536
  ### 업데이트 방법
538
537
 
539
538
  npm에 새 버전이 올라와도 설치본이나 실행 중인 MCP가 자동 교체되지는 않습니다.
540
539
 
541
- - **버전 고정**: 이 문서의 `tossinvest-mcp@0.3.0`은 해당 버전을 유지합니다. 업데이트하려면 버전을 직접 바꾸세요.
542
540
  - **최신 버전 확인 후 실행**: `args`를 `["-y", "--prefer-online", "tossinvest-mcp@latest"]`로 설정하면 다음 실행 시 최신 태그를 확인하는 방식으로 사용할 수 있습니다.
543
541
  - **전역·프로젝트 설치본**: 업데이트하거나 재설치해야 합니다. 단순한 `npx tossinvest-mcp`는 로컬 설치본·캐시·npm 설정의 영향을 받아 항상 최신이라고 보장하지 않습니다.
544
542
 
@@ -552,7 +550,7 @@ npm에 새 버전이 올라와도 설치본이나 실행 중인 MCP가 자동
552
550
 
553
551
  거래 기능을 켜고 주문 생성, 정정, 취소 도구를 사용하는 경우 실제 계좌에 실제 주문이 전송될 수 있습니다. 주문 전 종목, 가격, 수량, 주문 유형, 계좌를 반드시 직접 확인하세요.
554
552
 
555
- 현재 이 프로젝트는 개발중인 프로젝트로서, 이 프로젝트의 개발자와 배포자는 이 프로그램 사용으로 발생하는 투자 손실, 주문 실수, API 장애, 데이터 오류, 클라이언트 오작동, AI 응답 오류 등에 대해 책임지지 않습니다.
553
+ 현재 이 프로젝트는 개발중인 프로젝트로서, 이 프로젝트의 개발자와 배포자는 이 프로그램 사용으로 발생하는 투자 손실, 주문 실수, API 장애, 데이터 오류, 클라이언트 오작동, AI 응답 오류. 본 프로그램 오류 등에 대해 책임지지 않습니다.
556
554
 
557
555
  ## 라이선스
558
556
 
package/dist/server.js CHANGED
@@ -8,7 +8,7 @@ import { registerOrderTools } from "./tools/orders.js";
8
8
  export function createServer(config) {
9
9
  const server = new McpServer({
10
10
  name: "tossinvest-mcp",
11
- version: "0.3.0",
11
+ version: "0.3.2",
12
12
  });
13
13
  const client = new TossClient(config);
14
14
  setServerWarnings(server, config.warnings ?? []);
@@ -1,6 +1,8 @@
1
+ import { z } from "zod";
1
2
  import { resolveAccountSeq } from "../config.js";
2
- import { accountInputSchema, currencySchema, symbolSchema, } from "../toss/schemas.js";
3
+ import { accountInputSchema, currencySchema, dateSchema, symbolSchema, } from "../toss/schemas.js";
3
4
  import { runTool, registrar } from "./common.js";
5
+ const DEFAULT_TRANSACTIONS_LIMIT = 10;
4
6
  export function registerAccountTools(server, client, config) {
5
7
  const register = registrar(server);
6
8
  register("toss_get_accounts", {
@@ -10,7 +12,7 @@ export function registerAccountTools(server, client, config) {
10
12
  }, () => runTool(() => client.request({ path: "/api/v1/accounts" })));
11
13
  register("toss_get_holdings", {
12
14
  title: "Toss Get Holdings",
13
- description: "Get account holdings. accountSeq is optional when TOSSINVEST_ACCOUNT is configured.",
15
+ description: "Get account holdings and valuation; optionally filter by symbol.",
14
16
  inputSchema: {
15
17
  ...accountInputSchema(config),
16
18
  symbol: symbolSchema.optional(),
@@ -20,6 +22,61 @@ export function registerAccountTools(server, client, config) {
20
22
  accountSeq: resolveAccountSeq(accountSeq, config.defaultAccountSeq),
21
23
  query: { symbol },
22
24
  })));
25
+ register("toss_get_transactions", {
26
+ title: "Toss Get Transactions",
27
+ description: "Account trades, transfers, FX, dividends, interest and fees (KR/US stocks, options and bonds; see assetClass). Dates use KST settlement day (expected for unsettled); omit the to filter to include future settlements. Unsettled first, newest first within each group. Follow hasNext even on empty pages; pass nextCursor unchanged and clear it if account or filters change. No order IDs.",
28
+ inputSchema: {
29
+ ...accountInputSchema(config),
30
+ transactionTypes: z
31
+ .array(z.enum([
32
+ "BUY",
33
+ "SELL",
34
+ "SECURITIES_TRANSFER_IN",
35
+ "SECURITIES_TRANSFER_OUT",
36
+ "DEPOSIT",
37
+ "WITHDRAWAL",
38
+ "EXCHANGE_IN",
39
+ "EXCHANGE_OUT",
40
+ "DIVIDEND",
41
+ "INTEREST",
42
+ "FEE",
43
+ ]))
44
+ .optional()
45
+ .describe("OR filter. Omit or pass an empty array for all transaction types."),
46
+ from: dateSchema
47
+ .optional()
48
+ .describe("Inclusive settlement start date (KST)."),
49
+ to: dateSchema
50
+ .optional()
51
+ .describe("Inclusive settlement end date (KST), including future dates."),
52
+ cursor: z
53
+ .string()
54
+ .min(1)
55
+ .optional()
56
+ .describe("Pass nextCursor unchanged."),
57
+ limit: z
58
+ .number()
59
+ .int()
60
+ .min(1)
61
+ .max(100)
62
+ .optional()
63
+ .describe(`Page size; defaults to ${DEFAULT_TRANSACTIONS_LIMIT}, maximum 100. Set the needed page size on the first call.`),
64
+ },
65
+ }, ({ accountSeq, transactionTypes, from, to, cursor, limit }) => runTool(() => {
66
+ if (from !== undefined && to !== undefined && from > to)
67
+ throw new Error("from must be on or before to (settlement dates in KST).");
68
+ return client.request({
69
+ path: "/api/v1/transactions",
70
+ accountSeq: resolveAccountSeq(accountSeq, config.defaultAccountSeq),
71
+ query: {
72
+ transactionTypes: transactionTypes?.join(","),
73
+ from,
74
+ to,
75
+ cursor,
76
+ limit: limit ?? DEFAULT_TRANSACTIONS_LIMIT,
77
+ },
78
+ });
79
+ }));
23
80
  register("toss_get_buying_power", {
24
81
  title: "Toss Get Buying Power",
25
82
  description: "Get available buying power for an account. currency is required: KRW for Korean stocks, USD for US stocks.",
@@ -1,11 +1,14 @@
1
1
  import { z } from "zod";
2
- import { dateSchema } from "../toss/schemas.js";
2
+ import { dateSchema, dateTimeSchema } from "../toss/schemas.js";
3
3
  import { registrar, runTool } from "./common.js";
4
+ const DEFAULT_RANKINGS_COUNT = 20;
5
+ const DEFAULT_INVESTOR_RANKINGS_COUNT = 10;
6
+ const DEFAULT_INDICATOR_CANDLES_COUNT = 20;
4
7
  export function registerExtendedMarketTools(server, client) {
5
8
  const register = registrar(server);
6
9
  register("toss_list_stocks", {
7
10
  title: "마켓별 전체 종목 조회",
8
- description: "마켓별 전체 종목 조회. 마켓(거래소)별 종목 리스트를 조회합니다. 종목 유니버스 구성 용도이며, 토스증권에서 거래 가능한 종목만 제공합니다.\n`status`·`securityType`·`commonShare` 로 유니버스를 필터링할 수 있습니다.\n\n**정렬**: `symbol` 오름차순. 필터 조건에 해당하는 종목을 페이지네이션 없이 한 번에 반환합니다.\n\n**응답 규모**: 마켓당 최대 수천 건(예: NASDAQ 약 2,800건, gzip 응답 약 30KB). 일 배치로 갱신되는 저변동 데이터이므로 하루 1회 조회 후 로컬 캐싱을 권장합니다.\n\n**관련 API**: 여기서 얻은 `symbol` 로 [`GET /api/v1/stocks`](#operation/getStocks)(다건 상세 조회)를 호출해 종목명·통화·상장상태 등 상세 정보를 채울 수 있습니다.\n\n**Rate Limits Group**: `STOCK_ALL`\n",
11
+ description: "거래소별 토스 거래 가능 종목 전체를 심볼순으로 반환합니다. 페이지 없음; status·securityType·commonShare로 좁히세요. 개별 종목 상세는 toss_get_stocks.",
9
12
  inputSchema: {
10
13
  market: z.enum([
11
14
  "KOSPI",
@@ -31,9 +34,13 @@ export function registerExtendedMarketTools(server, client) {
31
34
  ])
32
35
  .optional(),
33
36
  commonShare: z.boolean().optional(),
37
+ symbolsOnly: z
38
+ .boolean()
39
+ .optional()
40
+ .describe("true이면 전체 결과를 심볼 문자열 배열로 반환합니다. 종목 정보도 필요하면 생략하세요."),
34
41
  },
35
42
  }, (input) => runTool(async () => {
36
- return client.request({
43
+ const response = await client.request({
37
44
  path: `/api/v1/stocks/all`,
38
45
  query: {
39
46
  market: input.market,
@@ -42,10 +49,24 @@ export function registerExtendedMarketTools(server, client) {
42
49
  commonShare: input.commonShare,
43
50
  },
44
51
  });
52
+ if (!input.symbolsOnly)
53
+ return response;
54
+ if (!Array.isArray(response.result))
55
+ throw new Error("Toss returned an invalid stock list.");
56
+ const symbols = response.result.map((stock) => {
57
+ if (!stock ||
58
+ typeof stock !== "object" ||
59
+ !("symbol" in stock) ||
60
+ typeof stock.symbol !== "string" ||
61
+ stock.symbol.length === 0)
62
+ throw new Error("Toss returned a stock without a valid symbol.");
63
+ return stock.symbol;
64
+ });
65
+ return { ...response, result: symbols };
45
66
  }));
46
67
  register("toss_get_stock_investor_trading", {
47
68
  title: "투자자별 매매동향 조회",
48
- description: "투자자별 매매동향 조회. 국내(KR) 종목의 투자자별 매매동향을 일별 거래량 시계열로 조회합니다. 개인·외국인·기관·기타법인\n4개 투자자 분류의 매수·매도·순매수 거래량을 최신순으로 제공하며, 기관은 7개 세부 분류(`breakdown`)를\n함께 제공합니다. 외국인 보유 현황(`foreignerHolding`)과 CFD 잔고(`cfd`)도 일자별로 포함합니다.\n\n- 국내(KR) 종목만 지원합니다. 다른 시장의 종목은 400 `unsupported-market` 으로 응답합니다.\n- 거래량은 KRX·NXT 통합 기준이며, 모든 수량은 주식 수(주) 정수입니다. 순매수(`netBuyVolume`)는\n 매수 − 매도로 계산되며 음수면 순매도입니다. 거래대금(금액) 축은 제공하지 않습니다.\n- `foreigner` 는 등록외국인 기준입니다. 시장 지표의 투자자별 매매대금\n (`GET /api/v1/market-indicators/{symbol}/investor-trading`, 등록·미등록 합계)과 기준이 다릅니다.\n- 당일 기록은 장중 잠정치로 제공합니다. 잠정치가 제공되지 않는 `individual`(개인)·\n `institution.breakdown`(기관 세부)·`otherCorporation`(기타법인)·`foreignerHolding`(외국인 보유)·\n `cfd`(CFD 잔고)는 null 이며, `updatedAt` 으로 마지막 갱신 시각을 확인할 수 있습니다.\n- 다음 페이지는 응답의 `nextUntil` 값을 `until` 파라미터로 전달해 조회합니다.\n\n**데이터 적시성**: 하나의 일자 기록은 시간에 걸쳐 완성됩니다. 투자자별 매매동향 확정치와\n외국인 보유(`foreignerHolding`)는 해당 일자 저녁에 반영됩니다. CFD 잔고(`cfd`)는 다음 영업일 새벽(T+1)에\n반영되며, 외국인 보유는 다음 영업일 오전에 확정치로 한 번 더 갱신될 수 있습니다.\n`updatedAt` 은 이 모든 반영을 포함한 기록 전체의 마지막 갱신 시각입니다.\n\n**Rate Limits Group**: `STOCK_TRADING_TREND`\n",
69
+ description: "국내 종목의 투자자별 일별 매수·매도·순매수 거래량(주). KRX·NXT 합산, 외국인은 등록 기준. 기관 세부·외국인 보유·CFD 포함. 장중 잠정치에서 미제공 항목은 null; updatedAt 확인. 다음 페이지: nextUntil → until.",
49
70
  inputSchema: {
50
71
  symbol: z.string().min(1).regex(new RegExp("^[A-Za-z0-9.\\-]+$")),
51
72
  count: z.number().int().min(1).max(100).optional(),
@@ -59,7 +80,7 @@ export function registerExtendedMarketTools(server, client) {
59
80
  }));
60
81
  register("toss_get_stock_program_trades", {
61
82
  title: "프로그램매매 동향 조회",
62
- description: "프로그램매매 동향 조회. 국내(KR) 종목의 프로그램매매 동향을 일별 거래량 시계열로 조회합니다. 차익거래(`arbitrage`)·\n비차익거래(`nonArbitrage`) 각각의 매수·매도·순매수 거래량을 최신순으로 제공합니다.\n\n- 국내(KR) 종목만 지원합니다. 다른 시장의 종목은 400 `unsupported-market` 으로 응답합니다.\n- KRX 시장 거래만 집계하며 NXT 거래는 포함하지 않습니다. 모든 수량은 주식 수(주) 정수입니다.\n 순매수(`netBuyVolume`)는 매수 − 매도로 계산되며 음수면 순매도입니다. 거래대금(금액) 축은 제공하지 않습니다.\n- 다음 페이지는 응답의 `nextUntil` 값을 `until` 파라미터로 전달해 조회합니다.\n\n**데이터 적시성**: 당일 기록도 제공되며, 장 종료 전까지 갱신될 수 있습니다.\n\n**Rate Limits Group**: `STOCK_TRADING_TREND`\n",
83
+ description: "국내 종목의 차익·비차익 프로그램 일별 매수·매도·순매수 거래량(주). KRX만 집계하며 당일은 변동 가능. 다음 페이지: nextUntil → until.",
63
84
  inputSchema: {
64
85
  symbol: z.string().min(1).regex(new RegExp("^[A-Za-z0-9.\\-]+$")),
65
86
  count: z.number().int().min(1).max(100).optional(),
@@ -73,7 +94,7 @@ export function registerExtendedMarketTools(server, client) {
73
94
  }));
74
95
  register("toss_get_stock_short_selling", {
75
96
  title: "공매도 동향 조회",
76
- description: "공매도 동향 조회. 국내(KR) 종목의 공매도 동향을 일별 시계열로 조회합니다. 공매도 거래량·거래대금과 함께\n해당 일자 전체 거래량·거래대금 대비 공매도 비중을 최신순으로 제공합니다.\n\n- 국내(KR) 종목만 지원합니다. 다른 시장의 종목은 400 `unsupported-market` 으로 응답합니다.\n- 모든 금액은 원화(KRW)이며, 별도의 통화 필드는 제공하지 않습니다.\n- 비중(`shortSellingVolumeRate`·`shortSellingAmountRate`)의 분모는 정규장 외 세션\n (장전·장후 시간외종가, 애프터마켓)을 포함한 해당 일자 누적 거래량·거래대금입니다.\n 분모 데이터가 없는 날짜는 비중이 null, 분모가 0 이면 `0` 입니다.\n- 다음 페이지는 응답의 `nextUntil` 값을 `until` 파라미터로 전달해 조회합니다.\n\n**데이터 적시성**: 일별 확정치는 해당 일자 저녁에 반영됩니다.\n\n**Rate Limits Group**: `STOCK_TRADING_TREND`\n",
97
+ description: "국내 종목의 일별 공매도 거래량·거래대금(KRW)·비중. 비중의 분모는 시간외 포함 당일 거래량·거래대금이며 미제공은 null. 다음 페이지: nextUntil → until.",
77
98
  inputSchema: {
78
99
  symbol: z.string().min(1).regex(new RegExp("^[A-Za-z0-9.\\-]+$")),
79
100
  count: z.number().int().min(1).max(100).optional(),
@@ -87,7 +108,7 @@ export function registerExtendedMarketTools(server, client) {
87
108
  }));
88
109
  register("toss_get_stock_credit_trades", {
89
110
  title: "신용거래 동향 조회",
90
- description: "신용거래 동향 조회. 국내(KR) 종목의 신용거래 동향을 일별 시계열로 조회합니다. 신용융자(`marginLoan`)·신용대주(`stockLoan`)\n각각의 신규·상환·잔고 수량과 잔고 비율·공여율을 최신순으로 제공합니다.\n\n- 국내(KR) 종목만 지원합니다. 다른 시장의 종목은 400 `unsupported-market` 으로 응답합니다.\n- 자기신용과 유통금융을 합산한 값이며, 모든 수량은 주식 수(주) 정수입니다.\n- `stockLoan`(신용대주)은 주식을 빌려 매도하는 개인 신용거래입니다. 기관 간 대차거래\n (`GET /api/v1/stocks/{symbol}/securities-lending`)와는 다른 데이터입니다.\n- 해당 일자에 융자·대주 중 한쪽 데이터만 있으면 없는 쪽 객체는 null 입니다.\n- 다음 페이지는 응답의 `nextUntil` 값을 `until` 파라미터로 전달해 조회합니다.\n\n**데이터 적시성**: 다음 영업일 새벽에 반영됩니다(T+1). 최신 기록은 전 영업일입니다.\n\n**Rate Limits Group**: `STOCK_TRADING_TREND`\n",
111
+ description: "국내 종목의 신용융자·신용대주 일별 수량(주)과 비율. 기관 간 대차와 별개. 미제공 항목은 null, 최신 기록은 전 영업일. 다음 페이지: nextUntil → until.",
91
112
  inputSchema: {
92
113
  symbol: z.string().min(1).regex(new RegExp("^[A-Za-z0-9.\\-]+$")),
93
114
  count: z.number().int().min(1).max(100).optional(),
@@ -101,7 +122,7 @@ export function registerExtendedMarketTools(server, client) {
101
122
  }));
102
123
  register("toss_get_stock_securities_lending", {
103
124
  title: "대차거래 동향 조회",
104
- description: "대차거래 동향 조회. 국내(KR) 종목의 대차거래 동향을 일별 시계열로 조회합니다. 대차 체결·상환·잔고 수량과\n잔고 금액을 최신순으로 제공합니다.\n\n- 국내(KR) 종목만 지원합니다. 다른 시장의 종목은 400 `unsupported-market` 으로 응답합니다.\n- 모든 금액은 원화(KRW)이며, 별도의 통화 필드는 제공하지 않습니다.\n- 대차거래는 기관 투자자 간 주식 대여·차입 거래입니다. 주식을 빌려 매도하는 개인 신용거래인\n 신용대주(`GET /api/v1/stocks/{symbol}/credit-trades` 의 `stockLoan`)와는 다른 데이터입니다.\n- 다음 페이지는 응답의 `nextUntil` 값을 `until` 파라미터로 전달해 조회합니다.\n\n**데이터 적시성**: 일별 확정치는 해당 일자 저녁에 반영됩니다.\n\n**Rate Limits Group**: `STOCK_TRADING_TREND`\n",
125
+ description: "국내 종목의 기관 간 일별 대차 체결·상환·잔고(수량·KRW). 신용대주와 별개. 다음 페이지: nextUntil → until.",
105
126
  inputSchema: {
106
127
  symbol: z.string().min(1).regex(new RegExp("^[A-Za-z0-9.\\-]+$")),
107
128
  count: z.number().int().min(1).max(100).optional(),
@@ -115,7 +136,7 @@ export function registerExtendedMarketTools(server, client) {
115
136
  }));
116
137
  register("toss_get_rankings", {
117
138
  title: "주식 랭킹 조회",
118
- description: "주식 랭킹 조회. 지정한 시장(`marketCountry`) · 기간(`duration`) · 기준(`type`)의 주식 랭킹을 조회합니다. 상위 100위까지 제공합니다.\n\n- `TOP_GAINERS` / `TOP_LOSERS` 는 `duration=realtime` 을 지원하지 않습니다 (400 `unsupported-ranking-duration`).\n- `tradingVolume` / `tradingAmount` 의 집계 기준은 `type` 이 결정합니다 — `TOSS_SECURITIES_*` 는 토스증권 체결 기준, 그 외(`MARKET_*` / `TOP_*`)는 시장 전체 기준.\n- `price.basePrice` 는 `TOP_GAINERS` / `TOP_LOSERS` 만 `duration` 시작 시점 기준가이며, 나머지 타입은 `duration` 과 무관하게 항상 전일 기준가입니다. `price.changeRate` 도 같은 의미를 따릅니다 (기간 등락률 vs 전일 대비 등락률).\n- 응답 항목 수는 `count` 보다 적을 수 있습니다 (시세 조회에 실패한 종목은 제외).\n- 랭킹이 집계되지 않은 조합은 에러가 아닌 빈 `rankings` 배열로 응답하며, 이때 `rankedAt` 은 null 입니다.\n\n**Rate Limits Group**: `RANKING`\n",
139
+ description: `국내·미국 주식의 시장·기간별 랭킹. count 기본 ${DEFAULT_RANKINGS_COUNT}, 최대 100; 첫 호출에 필요한 건수 지정. TOP_GAINERS/TOP_LOSERS는 realtime 불가. TOSS_SECURITIES_*는 토스 체결 기준. 급등락 기준가는 기간 시작가, 그 외는 전일 기준가. 데이터가 없으면 빈 목록.`,
119
140
  inputSchema: {
120
141
  type: z.enum([
121
142
  "MARKET_TRADING_AMOUNT",
@@ -141,13 +162,23 @@ export function registerExtendedMarketTools(server, client) {
141
162
  marketCountry: input.marketCountry,
142
163
  duration: input.duration,
143
164
  excludeInvestmentCaution: input.excludeInvestmentCaution,
144
- count: input.count,
165
+ count: input.count ?? DEFAULT_RANKINGS_COUNT,
145
166
  },
146
167
  });
147
168
  }));
169
+ register("toss_get_investor_trading_rankings", {
170
+ title: "투자자별 순매수·순매도 랭킹 조회",
171
+ description: `국내 주식·ETF의 외국인·기관·개인 순매수/순매도 랭킹(KRW). 최근 영업일만. count는 투자자·방향별 적용(기본 ${DEFAULT_INVESTOR_RANKINGS_COUNT}, 최대 100); 첫 호출에 필요한 건수 지정. 투자자별 rankedAt 확인; 장중 개인은 전 영업일일 수 있습니다. 시세 미포함.`,
172
+ inputSchema: {
173
+ count: z.number().int().min(1).max(100).optional(),
174
+ },
175
+ }, ({ count }) => runTool(() => client.request({
176
+ path: "/api/v1/rankings/investor-trading",
177
+ query: { count: count ?? DEFAULT_INVESTOR_RANKINGS_COUNT },
178
+ })));
148
179
  register("toss_get_market_indicator_prices", {
149
180
  title: "시장 지표 현재가 조회",
150
- description: "시장 지표 현재가 조회. 시장 지표(국내 지수·국채)의 현재가를 조회합니다. 최대 200건 까지 다건 조회를 지원하며 콤마(`,`)로 구분합니다.\n\n지원 심볼은 그룹 상단 **Market Indicators** 설명의 심볼 카탈로그(8종)를 따르며, 카탈로그에 없는 심볼은\n400 `unsupported-symbol` 로 응답합니다.\n\n**Rate Limits Group**: `MARKET_INDICATOR`\n",
181
+ description: "국내 지수(KOSPI·KOSDAQ)와 한국 국채(KR_BOND_*) 현재가. 지수는 포인트, 국채는 수익률(%). symbols는 쉼표 구분, 최대 200개.",
151
182
  inputSchema: {
152
183
  symbols: z
153
184
  .string()
@@ -162,12 +193,12 @@ export function registerExtendedMarketTools(server, client) {
162
193
  }));
163
194
  register("toss_get_market_indicator_candles", {
164
195
  title: "시장 지표 캔들 차트 조회",
165
- description: "시장 지표 캔들 차트 조회. 시장 지표(국내 지수·국채)의 캔들(OHLCV) 차트 데이터를 조회합니다. 최대 200개 봉을 반환합니다.\n\n봉은 최신순(`timestamp` 내림차순)으로 정렬됩니다 — 배열의 첫 요소가 가장 최근 봉, 마지막 요소가 가장 오래된 봉입니다.\n\n지원 심볼은 그룹 상단 **Market Indicators** 설명의 심볼 카탈로그(8종)와 동일하며, 카탈로그에 없는 심볼은\n400 `unsupported-symbol` 로 응답합니다. 개별 종목의 캔들은 `GET /api/v1/candles` 를 사용하세요.\n\n분봉(`1m`)은 지수(`KOSPI`·`KOSDAQ`)만 지원합니다. 국채(`KR_BOND_*`)는 일봉(`1d`)만 지원하며, 분봉 요청 시\n400 `invalid-request` 로 응답합니다.\n\n**Rate Limits Group**: `MARKET_INDICATOR_CHART`\n",
196
+ description: `국내 지수·국채 OHLCV, 최신순. 지수만 분봉 지원; 국채는 일봉만. count 기본 ${DEFAULT_INDICATOR_CANDLES_COUNT}, 최대 200; 첫 호출에 필요한 봉 수 지정. nextBefore를 시간대 포함 ISO 8601 값 그대로 before에 전달.`,
166
197
  inputSchema: {
167
198
  symbol: z.string().min(1).regex(new RegExp("^[A-Za-z0-9_]+$")),
168
199
  interval: z.enum(["1m", "1d"]),
169
200
  count: z.number().int().min(1).max(200).optional(),
170
- before: z.string().datetime({ offset: true }).optional(),
201
+ before: dateTimeSchema.optional(),
171
202
  },
172
203
  }, (input) => runTool(async () => {
173
204
  if (input.symbol.startsWith("KR_BOND_") && input.interval === "1m")
@@ -176,14 +207,14 @@ export function registerExtendedMarketTools(server, client) {
176
207
  path: `/api/v1/market-indicators/${encodeURIComponent(input.symbol)}/candles`,
177
208
  query: {
178
209
  interval: input.interval,
179
- count: input.count,
210
+ count: input.count ?? DEFAULT_INDICATOR_CANDLES_COUNT,
180
211
  before: input.before,
181
212
  },
182
213
  });
183
214
  }));
184
215
  register("toss_get_market_indicator_investor_trading", {
185
216
  title: "투자자별 매매대금 조회",
186
- description: "투자자별 매매대금 조회. KRX 시장(코스피·코스닥)의 투자자별 매매대금을 조회합니다. 개인·외국인·기관·기타법인 4개 투자자 분류의\n매수·매도 거래대금을 집계 단위(`interval`)별 기록으로 최신순 제공하며, 기관은 7개 세부 분류(`breakdown`)를\n함께 제공합니다.\n\n- `KOSPI` / `KOSDAQ` 만 지원합니다. 그 외 심볼은 400 `unsupported-symbol` 로 응답합니다.\n- 모든 거래대금은 원화(KRW) 정수이며, 별도의 통화 필드는 제공하지 않습니다.\n- 4개 분류(개인·외국인·기관·기타법인)의 매수 합계와 매도 합계는 시장 전체 기준으로 서로 같습니다.\n- `foreigner` 는 외국인 전체 합계(등록·미등록 외국인 포함)이며, `institution` 의 `buyAmount`/`sellAmount` 는\n `breakdown` 7개 항목의 합과 일치합니다.\n- 당일 기록은 장 종료 전까지 갱신될 수 있는 잠정치입니다. `updatedAt` 으로 마지막 갱신 시각을 확인하세요.\n- 다음 페이지는 응답의 `nextUntil` 값을 `until` 파라미터로 전달해 조회합니다.\n\n**Rate Limits Group**: `MARKET_INDICATOR`\n",
217
+ description: "코스피·코스닥 투자자별 매수·매도 거래대금(KRW). 등록·미등록 외국인 합산, 기관 세부 분류 포함. 당일 잠정치; updatedAt 확인. 다음 페이지: nextUntil → until.",
187
218
  inputSchema: {
188
219
  symbol: z.enum(["KOSPI", "KOSDAQ"]),
189
220
  interval: z.enum(["1d", "1w", "1mo", "1y"]),
@@ -202,14 +233,14 @@ export function registerExtendedMarketTools(server, client) {
202
233
  }));
203
234
  register("toss_list_sectors", {
204
235
  title: "섹터 목록 조회",
205
- description: "섹터 목록 조회. 토스증권 섹터 분류 전체를 계층 정보와 함께 한 번에 반환합니다.\n\n- 섹터는 토스증권 자체 산업 분류로, 한 종목이 여러 섹터에 속할 수 있습니다.\n- `depth` 0 = 대분류, 1 = 중분류, 2 = 소분류이며, `parentSectorId` 로 상위 섹터를 따라가면 전체 계층을 만들 수 있습니다.\n- 정렬은 `depth` 오름차순, 같은 depth 안에서는 `sectorId` 오름차순(숫자 기준)입니다.\n- 자주 바뀌지 않으므로 하루 한 번만 조회해 저장해 두고 쓰는 것을 권장합니다. 다른 섹터 API 의 `sectorId` 입력값은 이 목록에서 확인합니다.\n\n**Rate Limits Group**: `SECTOR`\n",
236
+ description: "토스 섹터 분류 전체와 계층 정보(depth·parentSectorId)를 조회합니다. 다른 섹터 도구에서 반환된 sectorId를 사용하세요.",
206
237
  inputSchema: {},
207
238
  }, () => runTool(async () => {
208
239
  return client.request({ path: `/api/v1/sectors`, query: {} });
209
240
  }));
210
241
  register("toss_get_sector_rankings", {
211
242
  title: "섹터 랭킹 조회",
212
- description: "섹터 랭킹 조회. 지정한 시장(`marketCountry`) · 기간(`duration`) · 기준(`type`)의 섹터 랭킹을 조회합니다. 상위 100위까지 한 번에 제공합니다.\n\n- 대상은 대분류(`depth` 0)와 중분류(`depth` 1) 섹터이며, 소분류(`depth` 2)는 포함되지 않습니다.\n- 소속 종목이 너무 적거나 해당 시장·기간의 데이터가 없는 섹터는 랭킹에 나타나지 않습니다.\n- `TOP_GAINERS` 는 `changeRate`, `MARKET_TRADING_AMOUNT` 는 `tradingAmount` 가 랭킹 기준값입니다. `tradingAmount` · `marketCapitalization` 은 데이터가 없으면 null 이고, `changeRate` 는 항상 값이 있습니다.\n- 금액은 US 도 원화(KRW) 환산값이며 통화는 각 항목의 `currency` 로 표기됩니다.\n- 응답 항목 수는 100건보다 적을 수 있고, 데이터가 없는 조합은 에러가 아닌 빈 `rankings` 배열로 응답합니다.\n\n**Rate Limits Group**: `SECTOR_RANKING`\n",
243
+ description: "시장·기간별 섹터 등락률 또는 거래대금 랭킹. 대·중분류만, 최대 100위. 미국 금액도 KRW 환산; 데이터가 없으면 빈 목록.",
213
244
  inputSchema: {
214
245
  type: z.enum(["TOP_GAINERS", "MARKET_TRADING_AMOUNT"]),
215
246
  marketCountry: z.enum(["KR", "US"]),
@@ -227,7 +258,7 @@ export function registerExtendedMarketTools(server, client) {
227
258
  }));
228
259
  register("toss_get_sector", {
229
260
  title: "섹터 상세 조회",
230
- description: "섹터 상세 조회. 섹터의 기본 정보(이름 · 계층 · 한 줄 요약)와 지정한 시장(`marketCountry`) 기준 기간별 등락률을 조회합니다.\n\n- 기본 정보는 시장과 무관하며, `performance.changeRates` 만 `marketCountry` 기준입니다.\n- `changeRates` 는 `duration` 순서(`1d` → `1y`)로 항상 6건이며, 데이터가 없는 기간은 `changeRate` 가 null 입니다 (예: 해당 시장에 구성 종목이 없는 섹터).\n- 등락률은 소수비율입니다.\n- 목록(`GET /api/v1/sectors`)에 없는 `sectorId` 는 404 `sector-not-found` 로 응답합니다.\n\n**Rate Limits Group**: `SECTOR`\n",
261
+ description: "섹터 상세·계층 및 선택한 시장의 기간별 등락률(소수비율). 미제공 기간은 null. sectorId는 toss_list_sectors에서 확인하세요.",
231
262
  inputSchema: {
232
263
  sectorId: z.string().min(1).regex(new RegExp("^[A-Za-z0-9_-]{1,40}$")),
233
264
  marketCountry: z.enum(["KR", "US"]),
@@ -240,7 +271,7 @@ export function registerExtendedMarketTools(server, client) {
240
271
  }));
241
272
  register("toss_get_sector_stocks", {
242
273
  title: "섹터 구성 주식 조회",
243
- description: "섹터 구성 주식 조회. 섹터(하위 섹터 포함)에 속한 보통주 전체를 지정한 시장(`marketCountry`) 기준으로 시가총액 내림차순 반환합니다.\n\n- `volume` · `tradingAmount` 는 당일 누적값이며, 시세가 없는 종목은 null 입니다.\n- 금액은 US 종목도 원화(KRW) 환산값이며 통화는 각 항목의 `currency` 로 표기됩니다.\n- 상장폐지된 종목은 제외됩니다.\n- 목록(`GET /api/v1/sectors`)에 없는 `sectorId` 는 404 `sector-not-found` 로 응답합니다.\n\n**Rate Limits Group**: `SECTOR`\n",
274
+ description: "섹터와 하위 섹터의 보통주 전체를 선택한 시장 기준 시가총액순으로 조회합니다. 거래량·거래대금은 당일 누적, 미국 금액도 KRW 환산.",
244
275
  inputSchema: {
245
276
  sectorId: z.string().min(1).regex(new RegExp("^[A-Za-z0-9_-]{1,40}$")),
246
277
  marketCountry: z.enum(["KR", "US"]),
@@ -253,7 +284,7 @@ export function registerExtendedMarketTools(server, client) {
253
284
  }));
254
285
  register("toss_get_sector_etfs", {
255
286
  title: "섹터 관련 ETF 조회",
256
- description: "섹터 관련 ETF 조회. 이 섹터로 분류된 ETF 전체를 지정한 시장(`marketCountry`) 기준으로 반환합니다 (하위 섹터 미포함).\n\n- 직전 분기 시가총액이 큰 순서이며, 시가총액 정보가 없는 ETF 는 뒤에 옵니다.\n- 심볼과 이름만 제공합니다. 시세는 `GET /api/v1/prices`, 종목 기본 정보는 `GET /api/v1/stocks` 를 사용하세요.\n- 상장폐지된 ETF 는 제외됩니다.\n- 목록(`GET /api/v1/sectors`)에 없는 `sectorId` 는 404 `sector-not-found` 로 응답합니다.\n\n**Rate Limits Group**: `SECTOR`\n",
287
+ description: "해당 섹터의 ETF 심볼과 이름. 하위 섹터는 제외합니다. 시세는 toss_get_prices를 사용하세요.",
257
288
  inputSchema: {
258
289
  sectorId: z.string().min(1).regex(new RegExp("^[A-Za-z0-9_-]{1,40}$")),
259
290
  marketCountry: z.enum(["KR", "US"]),
@@ -1,6 +1,8 @@
1
1
  import { z } from "zod";
2
- import { currencySchema, dateSchema, symbolSchema, symbolsSchema, } from "../toss/schemas.js";
2
+ import { currencySchema, dateSchema, dateTimeSchema, symbolSchema, symbolsSchema, } from "../toss/schemas.js";
3
3
  import { runTool, registrar } from "./common.js";
4
+ const DEFAULT_TRADES_COUNT = 20;
5
+ const DEFAULT_CANDLES_COUNT = 20;
4
6
  export function registerMarketTools(server, client) {
5
7
  const register = registrar(server);
6
8
  register("toss_get_orderbook", {
@@ -19,12 +21,15 @@ export function registerMarketTools(server, client) {
19
21
  }, ({ symbols }) => runTool(() => client.request({ path: "/api/v1/prices", query: { symbols } })));
20
22
  register("toss_get_trades", {
21
23
  title: "Toss Get Trades",
22
- description: "Get recent trades for one KRX or US stock symbol. count defaults to the Toss API default and is capped at 50.",
24
+ description: `Get recent trades for one KRX or US stock symbol. Optional count defaults to ${DEFAULT_TRADES_COUNT} (max 50); set the needed count on the first call.`,
23
25
  inputSchema: {
24
26
  symbol: symbolSchema,
25
27
  count: z.number().int().min(1).max(50).optional(),
26
28
  },
27
- }, ({ symbol, count }) => runTool(() => client.request({ path: "/api/v1/trades", query: { symbol, count } })));
29
+ }, ({ symbol, count }) => runTool(() => client.request({
30
+ path: "/api/v1/trades",
31
+ query: { symbol, count: count ?? DEFAULT_TRADES_COUNT },
32
+ })));
28
33
  register("toss_get_price_limits", {
29
34
  title: "Toss Get Price Limits",
30
35
  description: "Get upper and lower price limits for one KRX or US stock symbol.",
@@ -34,17 +39,23 @@ export function registerMarketTools(server, client) {
34
39
  }, ({ symbol }) => runTool(() => client.request({ path: "/api/v1/price-limits", query: { symbol } })));
35
40
  register("toss_get_candles", {
36
41
  title: "Toss Get Candles",
37
- description: "Get 1-minute or daily OHLCV candles for one KRX or US stock symbol.",
42
+ description: `KR/US stock 1-minute or daily OHLCV. count defaults to ${DEFAULT_CANDLES_COUNT} (max 200); set it on the first call. Pass nextBefore as before (ISO 8601 with timezone).`,
38
43
  inputSchema: {
39
44
  symbol: symbolSchema,
40
45
  interval: z.enum(["1m", "1d"]),
41
46
  count: z.number().int().min(1).max(200).optional(),
42
- before: z.string().datetime({ offset: true }).optional(),
47
+ before: dateTimeSchema.optional(),
43
48
  adjusted: z.boolean().optional(),
44
49
  },
45
50
  }, ({ symbol, interval, count, before, adjusted }) => runTool(() => client.request({
46
51
  path: "/api/v1/candles",
47
- query: { symbol, interval, count, before, adjusted },
52
+ query: {
53
+ symbol,
54
+ interval,
55
+ count: count ?? DEFAULT_CANDLES_COUNT,
56
+ before,
57
+ adjusted,
58
+ },
48
59
  })));
49
60
  register("toss_get_stocks", {
50
61
  title: "Toss Get Stocks",
@@ -64,11 +75,11 @@ export function registerMarketTools(server, client) {
64
75
  })));
65
76
  register("toss_get_exchange_rate", {
66
77
  title: "Toss Get Exchange Rate",
67
- description: "Get KRW/USD or USD/KRW exchange rate. dateTime is optional ISO 8601.",
78
+ description: "Get KRW/USD or USD/KRW exchange rate. Optional dateTime is ISO 8601 with timezone.",
68
79
  inputSchema: {
69
80
  baseCurrency: currencySchema,
70
81
  quoteCurrency: currencySchema,
71
- dateTime: z.string().datetime({ offset: true }).optional(),
82
+ dateTime: dateTimeSchema.optional(),
72
83
  },
73
84
  }, ({ baseCurrency, quoteCurrency, dateTime }) => runTool(() => client.request({
74
85
  path: "/api/v1/exchange-rate",
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { resolveAccountSeq } from "../config.js";
3
3
  import { executeTrade, reconcileAccepted } from "../guard/index.js";
4
- import { accountInputSchema, accountDescription, dateSchema, orderCancelSchema, orderCreateSchema, orderCreateFields, orderModifySchema, symbolSchema, withoutKeys, conditionalFields, conditionalCreateSchema, validatedConditionalModifySchema, clientOrderIdSchema, } from "../toss/schemas.js";
4
+ import { accountInputSchema, dateSchema, orderCancelSchema, orderCreateSchema, orderCreateFields, orderModifySchema, symbolSchema, withoutKeys, conditionalFields, conditionalCreateSchema, validatedConditionalModifySchema, clientOrderIdSchema, } from "../toss/schemas.js";
5
5
  import { registrar, runTool } from "./common.js";
6
6
  export function registerOrderTools(server, client, config) {
7
7
  const register = registrar(server);
@@ -12,7 +12,9 @@ export function registerOrderTools(server, client, config) {
12
12
  const key = conditional ? "conditionalOrderId" : "orderId";
13
13
  register(conditional ? "toss_get_conditional_orders" : "toss_get_orders", {
14
14
  title: conditional ? "Toss Get Conditional Orders" : "Toss Get Orders",
15
- description: `List ${group}; preserve nextCursor/hasNext for pagination. OPEN regular orders ignore limit/cursor. ${accountDescription}`,
15
+ description: conditional
16
+ ? "List conditional orders. If hasNext, pass nextCursor as cursor."
17
+ : "List regular orders. If hasNext, pass nextCursor as cursor. OPEN ignores limit and cursor.",
16
18
  inputSchema: {
17
19
  ...account,
18
20
  status: z.enum(["OPEN", "CLOSED"]),
@@ -30,7 +32,7 @@ export function registerOrderTools(server, client, config) {
30
32
  })));
31
33
  register(conditional ? "toss_get_conditional_order" : "toss_get_order", {
32
34
  title: `Toss Get ${conditional ? "Conditional " : ""}Order`,
33
- description: `Fetch one ${group} by identifier. ${accountDescription}`,
35
+ description: `Get one ${conditional ? "conditional" : "regular"} order by ${key}.`,
34
36
  inputSchema: { ...account, [key]: z.string().min(1) },
35
37
  }, (input) => runTool(async () => {
36
38
  const accountSeq = resolveAccountSeq(input.accountSeq, config.defaultAccountSeq);
@@ -65,10 +67,11 @@ export function registerOrderTools(server, client, config) {
65
67
  }
66
68
  if (!config.tradingEnabled)
67
69
  return;
68
- const instructions = `Real order, no sandbox. ${accountDescription} confirmHighValueOrder requires the user's high-value authorization; never set it automatically to bypass a rejection. Local budget uses lastPrice with configurable +/- buffer, not guaranteed execution value. Daily amount/count windows reset at fixed KST 20:01 for KR and 10:01 for US. Count caps are independent per account and market (KRW/USD); each reset clears only that market's amount and count.`;
70
+ const unknownAcceptance = "If acceptance is unknown, check order status before retrying.";
71
+ const createRetry = "If acceptance is unknown, check orders; never resubmit with a new clientOrderId.";
69
72
  register("toss_create_order", {
70
73
  title: "Toss Create Order",
71
- description: `${instructions} Provide quantity XOR orderAmount (US MARKET). Fractional quantity: US MARKET SELL only, up to 6 places, regular session until one hour before close. OPG: KR; CLS: US LIMIT.`,
74
+ description: `Submit a real order. LIMIT needs integer quantity and a price; MARKET omits price. Use exactly one of quantity or US MARKET orderAmount; orderAmount forbids timeInForce. Fractional quantity: US MARKET SELL only, max 6 decimals, regular session until 1h before close. OPG: KR; CLS: US LIMIT. ${createRetry}`,
72
75
  inputSchema: { ...orderCreateFields, ...account },
73
76
  }, (input) => runTool(async () => {
74
77
  const parsed = orderCreateSchema.parse(input);
@@ -81,7 +84,7 @@ export function registerOrderTools(server, client, config) {
81
84
  }));
82
85
  register("toss_modify_order", {
83
86
  title: "Toss Modify Order",
84
- description: `${instructions} Replaces an order and returns a NEW orderId. KR requires quantity; US forbids quantity and uses unfilled quantity. orderType/price may default to existing values.`,
87
+ description: `Replace a real order; use the new orderId. KR requires quantity; US omits it and uses the unfilled quantity. orderType and LIMIT price can inherit existing values; MARKET omits price. ${unknownAcceptance}`,
85
88
  inputSchema: { ...orderModifySchema.shape, ...account },
86
89
  }, (input) => runTool(async () => {
87
90
  const parsed = orderModifySchema.parse(input);
@@ -95,7 +98,7 @@ export function registerOrderTools(server, client, config) {
95
98
  }));
96
99
  register("toss_cancel_order", {
97
100
  title: "Toss Cancel Order",
98
- description: `Cancel a real order; filled shares cannot be canceled. Cancellation operation ID can differ from the original. ${accountDescription}`,
101
+ description: "Cancel the remaining unfilled quantity of a real order. Cancellation ID may differ from the original order ID.",
99
102
  inputSchema: { ...orderCancelSchema.shape, ...account },
100
103
  }, (input) => runTool(async () => {
101
104
  const p = orderCancelSchema.parse(input);
@@ -107,10 +110,10 @@ export function registerOrderTools(server, client, config) {
107
110
  body: {},
108
111
  });
109
112
  }));
110
- const conditionalDescription = `${instructions} Conditional budget applies on registration/modification day only, not future trigger day. SINGLE=one leg; OCO=max leg; OTO=sum. Per-order cap applies per leg. Future child orders are not charged a second time. OCO/OTO require LIMIT. expireDate is at most one year from today (Toss validates session/trigger rules).`;
113
+ const conditionalRules = "SINGLE uses first only; OCO and OTO require LIMIT, with SELL/SELL or BUY/SELL respectively. LIMIT legs need orderPrice; MARKET omits it. expireDate: within one year. Budget applies at registration/change only (SINGLE one, OCO max, OTO sum; cap per leg).";
111
114
  register("toss_create_conditional_order", {
112
115
  title: "Toss Create Conditional Order",
113
- description: conditionalDescription,
116
+ description: `Register a real conditional order. ${conditionalRules} ${createRetry}`,
114
117
  inputSchema: {
115
118
  ...conditionalFields,
116
119
  symbol: symbolSchema,
@@ -129,7 +132,7 @@ export function registerOrderTools(server, client, config) {
129
132
  }));
130
133
  register("toss_modify_conditional_order", {
131
134
  title: "Toss Modify Conditional Order",
132
- description: `${conditionalDescription} Supply the entire replacement definition. Returns a NEW conditionalOrderId; use it subsequently.`,
135
+ description: `Replace the full definition of a real conditional order; use the new conditionalOrderId. ${conditionalRules} ${unknownAcceptance}`,
133
136
  inputSchema: {
134
137
  ...conditionalFields,
135
138
  conditionalOrderId: z.string().min(1),
@@ -148,7 +151,7 @@ export function registerOrderTools(server, client, config) {
148
151
  }));
149
152
  register("toss_cancel_conditional_order", {
150
153
  title: "Toss Cancel Conditional Order",
151
- description: `Cancel the conditional group. Triggered child orders require ordinary cancellation separately. ${accountDescription}`,
154
+ description: "Cancel a real conditional order group. Cancel any triggered child order separately.",
152
155
  inputSchema: { conditionalOrderId: z.string().min(1), ...account },
153
156
  }, (input) => runTool(() => executeTrade(client, config, {
154
157
  account: resolveAccountSeq(input.accountSeq, config.defaultAccountSeq),
@@ -6,7 +6,18 @@ export const symbolsSchema = z.string().refine((v) => {
6
6
  }, "Provide 1 to 200 comma-separated symbols, without empty entries.");
7
7
  export const accountSeqSchema = z.number().int().positive();
8
8
  export const currencySchema = z.enum(["KRW", "USD"]);
9
- export const dateSchema = z.iso.date();
9
+ // Advertise standard formats without copying Zod's long patterns to every tool.
10
+ // The same validators still run server-side.
11
+ const isoDate = z.iso.date();
12
+ const isoDateTime = z.string().datetime({ offset: true });
13
+ export const dateSchema = z
14
+ .string()
15
+ .refine((v) => isoDate.safeParse(v).success, "Use a valid YYYY-MM-DD date.")
16
+ .meta({ format: "date" });
17
+ export const dateTimeSchema = z
18
+ .string()
19
+ .refine((v) => isoDateTime.safeParse(v).success, "Use an ISO 8601 timestamp with Z or a UTC offset.")
20
+ .meta({ format: "date-time" });
10
21
  export const decimalSchema = z
11
22
  .string()
12
23
  .max(30)
@@ -25,8 +36,8 @@ export const clientOrderIdSchema = z
25
36
  export const highValueSchema = z
26
37
  .boolean()
27
38
  .optional()
28
- .describe("Toss requires true for orders it values at KRW 100 million or more. Set true only when the user has acknowledged and authorized that high-value order; never automatically enable it after an error.");
29
- export const accountDescription = "When accountSeq is required, call toss_get_accounts first. Select the user's intended account; if multiple accounts are ambiguous, ask the user.";
39
+ .describe("Toss requires true at KRW 100 million or more. Set true only after the user explicitly approves this high-value order. Never auto-confirm after rejection.");
40
+ const accountDescription = "Call toss_get_accounts first; ask the user if the intended account is unclear.";
30
41
  export function accountInputSchema(config) {
31
42
  return {
32
43
  accountSeq: config.defaultAccountSeq === undefined
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tossinvest-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Model Context Protocol (MCP) server for Toss Securities Open API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -43,7 +43,7 @@
43
43
  "@modelcontextprotocol/sdk": "^1.32.1",
44
44
  "decimal.js": "^10.6.0",
45
45
  "proper-lockfile": "^4.1.2",
46
- "zod": "^4.4.3"
46
+ "zod": "^4.6.7"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/node": "^24.0.0",