tossinvest-mcp
# tossinvest-mcp
토스증권 Open API 를 감싸는 개인 매매용 MCP 서버.
국내·미국 주식 시세, 종목 정보, 섹터, 본인 계좌를 조회한다.
- 기본: 시세·종목 조회(A)와 계좌 조회(B) — **읽기 전용 tool 34개**
- 선택: 주문·조건주문 생성·정정·취소(C) 6개 — `TOSSINVEST_ENABLE_ORDERS=1` 일 때만 등록된다 ([주문 tool](#주문-tool))
- 응답 데이터는 본인 매매 목적으로만 쓴다. 제3자 배포 금지 (`docs/upstream/faq.md`).
## 사전 준비
1. 토스증권 WTS → 설정 → Open API 에서 `client_id` / `client_secret` 을 발급한다.
2. 같은 화면의 **허용 IP 관리**에 서버를 돌릴 공인 IP 를 등록한다. 등록되지 않은 IP 는 403 이다.
3. macOS 키체인에 자격증명을 넣는다. `-w` 를 마지막에 두면 값을 프롬프트로 입력받아 셸 기록에 남지 않는다.
```sh
security add-generic-password -a "$USER" -s tossinvest-client-id -w
security add-generic-password -a "$USER" -s tossinvest-client-secret -w
```
## 실행
같은 코드를 두 방식으로 실행한다. 둘 다 토큰 캐시 파일을 공유한다.
### (a) stdio — Claude Code 세션마다 실행
프로젝트의 `.mcp.json`:
```json
{
"mcpServers": {
"tossinvest": {
"type": "stdio",
"command": "/bin/zsh",
"args": [
"-c",
"export TOSSINVEST_CLIENT_ID=\"$(security find-generic-password -s tossinvest-client-id -w)\" TOSSINVEST_CLIENT_SECRET=\"$(security find-generic-password -s tossinvest-client-secret -w)\"; exec <uv 절대경로> run --directory <tossinvest-mcp 절대경로> tossinvest-mcp"
],
"env": {
"TOSSINVEST_ACCOUNT_SEQ": "1"
}
}
}
}
```
`<uv 절대경로>` 는 `which uv` 로 확인해 넣는다. Claude Code 가 `zsh -c` 로 실행할 때는 PATH 가 터미널과 다를 수 있어 절대경로를 쓴다.
`<tossinvest-mcp 절대경로>` 는 이 저장소를 클론한 디렉터리다.
`TOSSINVEST_ACCOUNT_SEQ` 는 선택이다. 모르면 빼고 실행한 뒤 `list_accounts` tool 로 확인한다.
### (b) Docker — streamable-http 상시 실행
```sh
docker build -t tossinvest-mcp .
mkdir -p ~/.cache/tossinvest-mcp && chmod 700 ~/.cache/tossinvest-mcp
TOSSINVEST_CLIENT_ID="$(security find-generic-password -s tossinvest-client-id -w)" \
TOSSINVEST_CLIENT_SECRET="$(security find-generic-password -s tossinvest-client-secret -w)" \
docker run -d --name tossinvest-mcp --restart unless-stopped \
-p 127.0.0.1:8000:8000 \
-e TOSSINVEST_CLIENT_ID -e TOSSINVEST_CLIENT_SECRET \
-e TOSSINVEST_ACCOUNT_SEQ=1 \
-v "$HOME/.cache/tossinvest-mcp:/cache" \
tossinvest-mcp
```
- `-e NAME` (값 없이)로 넘기면 비밀값이 `docker run` 명령줄(`ps`)에 드러나지 않는다.
컨테이너 환경변수라 `docker inspect` 로는 보인다.
- **HTTP 서버에는 인증이 없다.** 계좌 정보가 나가므로 반드시 `127.0.0.1` 에만 바인드한다
(`-p 127.0.0.1:8000:8000`).
- 키체인 값이 바뀌면 컨테이너를 다시 만든다 (`docker rm -f tossinvest-mcp` 후 위 명령).
Claude Code 에서 연결:
```sh
claude mcp add --transport http tossinvest http://127.0.0.1:8000/mcp
```
### CLI 옵션 / 환경변수
| 환경변수 | CLI | 기본값 | 설명 |
|---|---|---|---|
| `TOSSINVEST_CLIENT_ID` | — | (필수) | 토스증권 client_id |
| `TOSSINVEST_CLIENT_SECRET` | — | (필수) | 토스증권 client_secret |
| `TOSSINVEST_ACCOUNT_SEQ` | — | 없음 | 계좌 tool 의 기본 `accountSeq`. tool 인자 `account_seq` 로 덮어쓴다 |
| `TOSSINVEST_ENABLE_ORDERS` | — | 꺼짐 | `1`/`true` 면 주문·조건주문 tool 6개를 등록한다 |
| `TOSSINVEST_TOKEN_CACHE` | — | `~/.cache/tossinvest-mcp/token.json` | 토큰 캐시 파일 (Docker 는 `/cache/token.json`) |
| `TOSSINVEST_MCP_TRANSPORT` | `--transport` | `stdio` (Docker `streamable-http`) | `stdio` 또는 `streamable-http` |
| `TOSSINVEST_MCP_HOST` | `--host` | `127.0.0.1` (Docker `0.0.0.0`) | HTTP 바인드 주소 |
| `TOSSINVEST_MCP_PORT` | `--port` | `8000` | HTTP 포트. 경로는 `/mcp` |
CLI 옵션이 환경변수보다 우선한다.
## 토큰
- access token 은 client 당 1개만 유효하고, 새로 발급하면 이전 토큰이 즉시 무효가 된다(`401 token-revoked`).
- 그래서 모든 프로세스가 토큰 캐시 파일(권한 600)을 공유하고, 발급은 파일 잠금 안에서만 한다.
- 만료 10분 전부터 미리 재발급한다. `401 expired-token` / `token-revoked` 를 받으면
캐시를 다시 읽거나 재발급한 뒤 한 번만 재시도한다.
- **주의 (macOS Docker Desktop):** 호스트와 컨테이너 사이에는 바인드 마운트의 파일 잠금이 전달되지 않는다.
stdio(호스트)와 Docker 를 동시에 쓰다가 두 쪽이 같은 순간 발급하면 한쪽 토큰이 무효가 될 수 있다.
이때는 위의 401 재시도로 캐시의 최신 토큰을 받아 복구된다.
## tool 목록
아래 34개는 읽기 전용(`readOnlyHint`)이다. 응답은 모두 compact JSON 이다.
가격·수량·금액은 문자열 숫자, 등락률·수익률·수수료율은 소수 비율(0.1 = 10%)이다.
### 시세·종목 (토큰만 필요)
| tool | 엔드포인트 | 비고 |
|---|---|---|
| `get_prices` | `GET /api/v1/prices` | 최대 200종목 |
| `get_orderbook` | `GET /api/v1/orderbook` | |
| `get_recent_trades` | `GET /api/v1/trades` | 최대 50건 |
| `get_price_limits` | `GET /api/v1/price-limits` | |
| `get_candles` | `GET /api/v1/candles` | 1m/1d, `before` 로 과거 페이지 |
| `get_stock_info` | `GET /api/v1/stocks` | 최대 200종목 |
| `list_market_stocks` | `GET /api/v1/stocks/all` | 당일 캐싱. `query` 로 이름·심볼 필터 |
| `get_stock_warnings` | `GET /api/v1/stocks/{symbol}/warnings` | |
| `get_investor_trading` | `GET /api/v1/stocks/{symbol}/investor-trading` | 국내 전용 |
| `get_program_trading` | `GET /api/v1/stocks/{symbol}/program-trades` | 국내 전용 |
| `get_short_selling` | `GET /api/v1/stocks/{symbol}/short-selling` | 국내 전용 |
| `get_credit_trading` | `GET /api/v1/stocks/{symbol}/credit-trades` | 국내 전용 |
| `get_securities_lending` | `GET /api/v1/stocks/{symbol}/securities-lending` | 국내 전용 |
| `get_exchange_rate` | `GET /api/v1/exchange-rate` | 기본 USD→KRW |
| `get_market_calendar` | `GET /api/v1/market-calendar/{KR,US}` | `market` 인자로 국내·미국 선택 |
| `get_stock_rankings` | `GET /api/v1/rankings` | |
| `get_investor_trading_rankings` | `GET /api/v1/rankings/investor-trading` | 국내 전용 |
| `get_market_indicator_prices` | `GET /api/v1/market-indicators/prices` | KOSPI·KOSDAQ·국채 6종 |
| `get_market_indicator_candles` | `GET /api/v1/market-indicators/{symbol}/candles` | |
| `get_market_investor_trading` | `GET /api/v1/market-indicators/{symbol}/investor-trading` | KOSPI·KOSDAQ |
| `list_sectors` | `GET /api/v1/sectors` | 당일 캐싱 |
| `get_sector` | `GET /api/v1/sectors/{sectorId}` | |
| `get_sector_stocks` | `GET /api/v1/sectors/{sectorId}/stocks` | |
| `get_sector_etfs` | `GET /api/v1/sectors/{sectorId}/etfs` | |
| `get_sector_rankings` | `GET /api/v1/sectors/rankings` | |
### 계좌 (토큰 + `X-Tossinvest-Account`)
| tool | 엔드포인트 | 비고 |
|---|---|---|
| `list_accounts` | `GET /api/v1/accounts` | 계좌 번호는 끝 4자리만 표시 |
| `get_holdings` | `GET /api/v1/holdings` | |
| `list_orders` | `GET /api/v1/orders` | `CLOSED` 는 `nextCursor` 로 페이지 |
| `get_order` | `GET /api/v1/orders/{orderId}` | |
| `get_buying_power` | `GET /api/v1/buying-power` | |
| `get_sellable_quantity` | `GET /api/v1/sellable-quantity` | |
| `get_commissions` | `GET /api/v1/commissions` | |
| `list_conditional_orders` | `GET /api/v1/conditional-orders` | |
| `get_conditional_order` | `GET /api/v1/conditional-orders/{conditionalOrderId}` | |
계좌 tool 은 모두 `account_seq` 인자(선택)를 받는다. 없으면 `TOSSINVEST_ACCOUNT_SEQ` 를 쓴다.
## 주문 tool
실제 자금이 움직이는 tool 이다. 기본은 꺼져 있고 `TOSSINVEST_ENABLE_ORDERS=1`(또는 `true`)일 때만
tools/list 에 나온다. `destructiveHint` 로 표시된다.
| tool | 엔드포인트 | 주요 인자 |
|---|---|---|
| `place_order` | `POST /api/v1/orders` | `symbol`, `side`, `order_type`, `quantity` 또는 `order_amount`, `price`, `time_in_force`, `client_order_id` |
| `modify_order` | `POST /api/v1/orders/{orderId}/modify` | `order_id`, `market`(KR/US, 검증용), `order_type`, `price`, `quantity`(국내만) |
| `cancel_order` | `POST /api/v1/orders/{orderId}/cancel` | `order_id` |
| `place_conditional_order` | `POST /api/v1/conditional-orders` | `symbol`, `type`(SINGLE/OCO/OTO), `quantity`, `order_type`, `expire_date`, `first`, `second`, `client_order_id` |
| `modify_conditional_order` | `POST /api/v1/conditional-orders/{id}/modify` | `conditional_order_id` + 생성과 같은 조건 전체. **새 ID 가 발급된다** |
| `cancel_conditional_order` | `DELETE /api/v1/conditional-orders/{id}` | `conditional_order_id` |
공통 인자: `dry_run`(기본 `true`), `account_seq`, 생성·정정에는 `confirm_high_value_order`(기본 `false`).
- **dry_run**: 기본값 `true` 에서는 API 를 부르지 않고 보낼 요청(메서드·경로·accountSeq·본문)과 로컬 검증
결과만 돌려준다. `dry_run=false` 를 명시해야 전송한다. LLM 에게는 dry_run 결과를 보여 주고 확인을 받은 뒤
전송하라고 tool 설명으로 안내한다.
- **clientOrderId**: 생성 tool 은 멱등키를 자동으로 넣고 dry_run 결과에 보여 준다. 그 값을 `client_order_id` 로
넘겨 전송하고, 응답을 못 받았으면 10분 안에 같은 값·같은 내용으로 다시 호출하면 중복 없이 처음 결과를 받는다.
- **재시도**: 429 만 자동 재시도한다. 네트워크 오류·타임아웃은 재시도하지 않고 "결과 불명" 에러로
상태 확인 방법(get_order, list_orders 등)을 알려 준다.
- **1억원 이상**: `confirm_high_value_order=true` 를 직접 넘겨야 한다. 자동으로 켜지 않는다.
- **로컬 검증**: quantity/order_amount 중 하나, LIMIT 가격 필수·MARKET 가격 불가, 금액 주문은 MARKET 만,
소수점 수량은 MARKET+SELL 만, 국내 정정은 quantity 필수·미국 정정은 quantity 불가, OCO/OTO 는 LIMIT 만,
OCO 는 둘 다 SELL·first 감시가 > second 감시가, OTO 는 BUY→SELL. 호가 단위·잔고·장 시간은 서버가 판단한다.
### 켜는 방법
stdio — `.mcp.json` 의 `env` 에 추가:
```json
"env": {
"TOSSINVEST_ACCOUNT_SEQ": "1",
"TOSSINVEST_ENABLE_ORDERS": "1"
}
```
Docker — `docker run` 에 `-e TOSSINVEST_ENABLE_ORDERS=1` 을 추가해 컨테이너를 다시 만든다.
```sh
docker rm -f tossinvest-mcp
TOSSINVEST_CLIENT_ID="$(security find-generic-password -s tossinvest-client-id -w)" \
TOSSINVEST_CLIENT_SECRET="$(security find-generic-password -s tossinvest-client-secret -w)" \
docker run -d --name tossinvest-mcp --restart unless-stopped \
-p 127.0.0.1:8000:8000 \
-e TOSSINVEST_CLIENT_ID -e TOSSINVEST_CLIENT_SECRET \
-e TOSSINVEST_ACCOUNT_SEQ=1 \
-e TOSSINVEST_ENABLE_ORDERS=1 \
-v "$HOME/.cache/tossinvest-mcp:/cache" \
tossinvest-mcp
```
### 권장 사항
- **필요할 때만 켠다.** 매매하지 않을 때는 `TOSSINVEST_ENABLE_ORDERS` 를 빼서 tool 자체가 보이지 않게 한다.
- **Claude Code 권한 설정에서 주문 tool 을 자동 허용(allow)하지 않는다.** `settings.json` 의 `permissions.allow` 에
`mcp__tossinvest__place_order` 같은 주문 tool 이나 `mcp__tossinvest` 전체를 넣지 말고, 호출마다 직접 승인한다.
dry_run 확인과 별개로 마지막 안전장치가 된다.
- 주문 tool 이 켜진 상태로 HTTP 서버를 외부에 열지 않는다 (`127.0.0.1` 바인드 유지).
## 개발
`DEVELOPMENT.md` 참고.
테스트는 `docs/upstream/openapi.json` 의 예제를 fixture 로 쓴다. `docs/` 는 git 에 넣지 않으므로
처음 클론한 뒤 테스트 전에 한 번 실행해 토스증권 공개 문서에서 받아 둔다.
```sh
uv run python scripts/fetch_openapi.py # docs/upstream/openapi.json 내려받기
uv run pytest
```
## 라이선스
MIT 라이선스 (`LICENSE` 참고).
토스증권 API 응답 데이터는 본인 매매 목적으로만 쓰고 제3자에게 배포할 수 없다는 토스증권 이용 정책은 이 코드의 라이선스와 별개로 적용된다.
TDQS
Scored across 34 tools
Most tools are cleanly separated by resource scope (single stock vs market-wide vs sector) and by data type (prices, candles, orderbook, trades, short selling, credit, lending). The riskiest cluster is the three 'rankings' tools (get_stock_rankings, get_investor_trading_rankings, get_sector_rankings) plus get_investor_trading vs get_market_investor_trading, but the descriptions explicitly state scope and clarify what only one tool returns (e.g., rankings are the only source of prior-close change rates). No two tools appear to do the same thing.
Names are uniformly snake_case with a predictable get_/list_ verb split (get_ for single/aggregate reads, list_ for collections), and market-data tools are systematically prefixed by domain (market_indicator_, sector_, investor_trading_*). A few minor deviations exist, such as get_prices (plural) alongside singular get_stock_info and list_market_stocks, but the pattern remains readable and predictable.
At 34 tools the surface is well beyond the 3-15 sweet spot and past even the heavy 16-25 band. Each tool covers a genuinely narrow endpoint (per-metric daily series for program trading, short selling, credit, lending), which forces the agent to scan a long list to pick the right one, even though nothing is strictly redundant.
The market-data side is thorough (quotes, candles, orderbook, fills, limits, rankings, indicators, sectors, investor/short/credit/lending flows) and the account side covers read workflows (accounts, holdings, orders, conditional orders, buying power, sellable qty, commissions). However, there are no write operations — no place/cancel/modify order and no conditional-order creation — which is a notable gap for a brokerage server, though it may be an intentional read-only design.