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.
| 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.
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.
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.
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.
detail_level="summary" is the default for the large portfolio tools. It moves ID lists, per-day FX tables and
evidence blocks to full, lists each moved path and its item count in meta.detail.omitted, and stores repeated
metadata once under shared. Numbers the answer needs, and reason codes that explain a missing value, are never
moved.get_portfolio_summary, get_portfolio_returns and get_holdings stay at or under 27,000
bytes on a multi-account test portfolio.get_transactions and
get_crypto_account_history. Pages hold 1–500 rows (default 100). A cursor is rejected if the arguments or the default date window changed.detail_level="full" returns the complete object.tests/test_merge_budget.py, tests/test_merge_paging.py and tests/test_route_returns_size.py enforce these
limits.
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).
/info
endpoint.snapshot_portfolio_nav, export_portfolio_dataset and
import_statement. Their descriptions tell the model to use them only on an explicit request, and they are
annotated readOnlyHint: false. import_statement stores nothing until a second call confirms the preview.
Every other tool is annotated readOnlyHint: true.127.0.0.1 only. If a hosted client needs to reach it, put your own HTTPS reverse proxy in
front and turn on OAuth with the owner passphrase gate (see .env.example).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 |
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.
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.