K-invest

한국어 · GitHub

K-invest is a read-only MCP server that lets an AI chat client such as Claude or ChatGPT answer questions about your own brokerage and crypto accounts. It is for individual investors in Korea with accounts at Toss Securities and/or Korea Investment & Securities (KIS). It puts holdings, period returns, trades and realized P&L in one place. Every number carries its source and as-of time, and a reason code where data is incomplete.

You run it on your own machine or server with your own API keys. It sends requests only to the APIs listed below.

Data sources

Source Access Used for
Toss Securities Open API official; your client ID and secret accounts, holdings, cash, order history, quotes, candles, market calendar, FX
KIS Open API official; your app key and secret domestic and overseas balances, cash, trade history, quotes
SEC EDGAR official; descriptive User-Agent in SEC_USER_AGENT US financials, TTM figures, Form 4 insider trades
Yahoo Finance via yfinance unofficial financials and key metrics, dividend calendars, the 10Y Treasury yield, fallback FX. Values are labelled yfinance.
Binance USD-M futures API official; read-only API key futures positions, balances, fills, funding. Public market data needs no key.
Hyperliquid info API official public endpoint; account address only perp positions, balances, fills, funding
Upbit public ticker official public endpoint KRW price of USDT/USDC for valuing crypto balances
Your own files local Toss PDF statements and Binance CSV exports, imported on request

It does not scrape or log in to any website. An account you do not configure is reported as not configured, and the other tools keep working.

Tools

Chat clients see 21 tools. The server registers 68. The other 47 are older, narrower tools that the 21 replace; they stay callable for scripts and local services.

Tool What it returns
get_status Connector status: cached service health, live source probes, configured accounts, or the usage guide
get_holdings Current positions merged per symbol in native currency and KRW, plus cash
get_portfolio_summary Totals across every broker account, by section: totals, exposure, concentration, realized P&L
get_portfolio_returns Return for a period: simple return, TWR and XIRR side by side, P&L split, NAV risk statistics
get_transactions Trades across every Toss and KIS account, newest first, one page at a time
get_trading_pnl Realized P&L under per-account moving-average cost basis, round trips, win rates
get_quote Quotes for up to 10 symbols (Toss, falling back to KIS)
get_candles OHLCV candles for one stock
get_market_info KR/US market sessions and holidays, an FX rate, the 10Y US Treasury yield
get_crypto_market Public Binance USD-M market data for one symbol
get_crypto_positions Open crypto futures positions and balances (Binance USD-M, Hyperliquid)
get_crypto_account_history Your crypto futures fills or funding payments, paged
get_stock_snapshot Quote, metrics, analyst consensus and insider data for one stock
get_financials, get_key_metrics Financial statements and key metrics from yfinance (US, Korea, Japan)
get_sec_financials, get_ttm_financials SEC annual and quarterly financials, TTM figures (US)
get_insider_trades SEC Form 4 insider transactions (US)
snapshot_portfolio_nav Local write. Stores today’s NAV snapshot. The 18:10 KST timer normally does this.
export_portfolio_dataset Local write. Freezes an auditable copy of the portfolio data and returns it in pages
import_statement Local write. Imports buy/sell rows from a Toss PDF statement in a private inbox folder into the local ledger, in two steps

Returns are computed from your own stored data. Where part of the history is missing, for example a deposit that no API reports, the tool states this with a reason code and marks the figure as an estimate.

Architecture

flowchart LR
    client["MCP client<br/>(Claude, ChatGPT)"] -->|"Streamable HTTP /mcp<br/>bearer token or OAuth 2.1"| auth
    subgraph server["server.py (127.0.0.1:8100)"]
        auth["auth<br/>static token, optional OAuth 2.1 + owner gate,<br/>loopback service token"]
        catalog["tools_v2<br/>21 chat tools over 68 registered"]
        shape["response_shape<br/>compact JSON text + structuredContent,<br/>detail_level summary / full"]
        engine["portfolio_engine, interval_returns<br/>valuation, cost basis, TWR / XIRR, flows"]
        sources["portfolio_sources<br/>per-account collection, FX, health"]
        store[("SQLite<br/>NAV snapshots, ledger, datasets")]
        auth --> catalog --> shape
        catalog --> engine --> sources
        engine --> store
    end
    sources --> toss["Toss Open API"]
    sources --> kis["KIS Open API"]
    catalog --> crypto["Binance USD-M,<br/>Hyperliquid, Upbit"]
    catalog --> research["SEC EDGAR,<br/>yfinance (unofficial)"]
    timers["systemd timers<br/>NAV 18:10 KST, ledger 04:10 KST,<br/>futures risk hourly"] --> store
    files["your Toss PDFs,<br/>Binance CSVs"] -->|"import on request"| store

The timers in deploy/ run scripts/snapshot_nav.py, scripts/collect_ledger.py and scripts/snapshot_futures_risk.py without the server. A return between two dates needs a stored NAV for each date, so the daily snapshot is what makes period returns possible. Crypto futures are reported separately and are not part of the portfolio NAV.

Response size budgets

Every tool result carries one structuredContent object and the same object as compact JSON in a single text block (MCP 2025-06-18). A client that shows the model only the text (claude.ai) and a client that reads structuredContent (ChatGPT) see the same fields.

tests/test_merge_budget.py, tests/test_merge_paging.py and tests/test_route_returns_size.py enforce these limits.

Cross-client regression

Before a release, a fixed set of 19 questions is asked in both Claude and ChatGPT against the same running server. Each answer is graded pass, partial or fail against the tool output it should be based on. Under the owner’s grading rule, ChatGPT may also use its own built-in tools, as long as the portfolio data comes from K-invest.

Latest run, on this release (2026-10-22 21:14 KST, 19 prompts per client):

Client Pass Partial Fail
Claude 18 1 0
ChatGPT 16 3 0

The offline test suite checks the wire format both clients depend on. The text block must equal structuredContent, and a whole-call failure must set isError. The chat catalog, descriptions and schemas are pinned by hash (tests/test_tx_wire.py, tests/test_merge_catalog.py, tests/test_tool_offloop.py).

Read-only safety

Quick start

Requires Python 3.10+ and at least one configured source.

git clone <this repository> k-invest && cd k-invest
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
pip install pytest pytest-asyncio exceptiongroup ruff
python -m pytest tests/ -q          # offline; no network, keys or accounts needed

cp .env.example .env                # fill in the sources you use
echo "MCP_AUTH_TOKEN=$(python -c 'import secrets; print(secrets.token_urlsafe(32))')" >> .env
python server.py                    # serves http://127.0.0.1:8100/mcp

Point an MCP client at http://127.0.0.1:8100/mcp with the header Authorization: Bearer <MCP_AUTH_TOKEN>, then call get_status to check the setup. The server refuses to start without MCP_AUTH_TOKEN.

For a server install, deploy/ has systemd units for the server and the three timers (replace youruser and the paths). Optional settings, all described in .env.example:

Variable Effect
TOSS_CREDS_FILE, KIS_ACCOUNTS_JSON several Toss or KIS accounts
BINANCE_FUTURES_API_KEY_FILE, BINANCE_FUTURES_SIGNING_KEY_FILE, HYPERLIQUID_ACCOUNT_ADDRESSES crypto futures accounts
MCP_OAUTH_ENABLED, MCP_PUBLIC_URL, OWNER_GATE_REDIRECT_ORIGINS OAuth 2.1 with dynamic client registration for hosted clients. Create the passphrase hash with python -m owner_gate hash.
KINVEST_PRODUCT_MAP_FILE look-through from leveraged or single-stock ETFs to their underlying (format in product_map.example.json). Without it every product counts as its own exposure.
KINVEST_STATEMENT_INBOX folder of Toss PDF statements for import_statement
KINVEST_PORTFOLIO_DB, KINVEST_CACHE_DIR where the SQLite store and caches live

Tests

python -m pytest tests/ -q runs the whole suite offline against synthetic data and mocked HTTP. A few tests compare against files from older releases in the git history. In a clone without that history they are skipped and the reason is printed.

License and support

MIT, see LICENSE.

This is a personal project published as-is. No support, maintenance or answers to issues are promised. It is not affiliated with Toss Securities, Korea Investment & Securities, Binance, Hyperliquid or Upbit. Figures are only as good as the data the APIs return, and yfinance is unofficial. Nothing this software outputs is investment advice.