AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
MCP verified MIT Self-run

Schwab Marketdata Mcp

mcp-kevinkda-schwab-marketdata-mcp · by kevinkda

Read-only MCP server for Charles Schwab Market Data Production API (14 tools, OAuth, DuckDB cache, OWASP-tested).

No reviews yet
0 installs
10 views
0.0% view→install

Install

$ agentstack add mcp-kevinkda-schwab-marketdata-mcp

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access Used
  • Shell / process execution No
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/mcp-kevinkda-schwab-marketdata-mcp)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
1mo ago

Declared compatibility

Claude CodeClaude DesktopCursorWindsurf

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Schwab Marketdata Mcp? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

schwab-marketdata-mcp

[English](./README.md) | [简体中文](./README_zh.md)

> v0.3 sprint (in flight) — Sprint A focuses on Windows Tier A real-hardware > verification, dependency hygiene (Dependabot), schwab-py drift log, and a > cumulative hour ledger. See: > > - [docs/WINDOWS_VERIFICATION.md](docs/WINDOWSVERIFICATION.md) — 8-criterion > acceptance checklist for a volunteer with a real Windows box. > - [docs/THREAT_MODEL.md §6.6](docs/THREATMODEL.md) — schwab-py upgrade > drift log (must be filled in before any schwab-py bump merges). > - [docs/HOURS.md](docs/HOURS.md) — cumulative hours vs the 480 h project > budget; current usage ≈ 5%.

Production-grade Model Context Protocol (MCP) server that exposes the Charles Schwab Market Data Production API as 17 tools (10 endpoints + 2 derived option-analytics + 3 meta tools + 1 cached-history analytics + 1 experimental streaming snapshot tool) for use inside Cursor, Claude Code, and any other MCP-aware agent.

> Read-only — this project calls only the Schwab Market Data API. It does > NOT call the Schwab Trader API and does NOT place orders. See > [Responsible Use](#responsible-use) for the Schwab Terms of Service impact.


Overview

schwab-marketdata-mcp is the server-side half of a two-repo system:

  • This repo — the MCP server. Owns OAuth, rate limiting, retry/backoff,

token rotation, structured error mapping, and stdio framing.

  • Companion repo — [schwab-marketdata-skill](../schwab-marketdata-skill)

ships two Cursor / Claude Skills that document how to call this server (single-tool ops skill and multi-step workflows skill).

The server wraps the unofficial schwab-py SDK with the production hardening required for an always-on MCP host:

  • Atomic refresh-token rotation with fcntl.flock (cross-process safe).
  • Token-on-disk permission audit (0600 enforced on every read).
  • Per-process rate limiter (default 120 req/min, configurable per call).
  • Adaptive 429 / 5xx retry with exponential backoff and Retry-After parsing.
  • Structured error hierarchy (SchwabAuthError, SchwabRateLimitError, etc.)

so agents can surface actionable messages instead of stack traces.

  • stdio hardening so log lines never corrupt the JSON-RPC stream.

See [docs/THREAT_MODEL.md](docs/THREAT_MODEL.md) for the full architecture and threat model.


Quick Start

> Step 0 (mandatory) — install pre-commit hooks before copying any > secrets into the working tree: > > ``bash > uv sync --extra dev > uv run pre-commit install > ``

# 1. Sync deps (uses the committed uv.lock)
uv sync --extra dev

# 2. Install pre-commit hooks (gitleaks + detect-secrets + ruff + mypy)
uv run pre-commit install

# 3. Configure your Schwab Developer Portal app credentials
cp .env.example .env
# then edit .env and replace `dummy-not-a-real-secret` placeholders with
# real values from https://developer.schwab.com/dashboard/apps

# 4. One-time OAuth login (browser opens; click through the self-signed cert)
uv run python -m schwab_marketdata_mcp.auth login_flow
# Or (containerized / headless): manual_flow
uv run python -m schwab_marketdata_mcp.auth manual_flow

# 5. Schedule the health probe (once per project, see docs/cron.example)
#    macOS: copy the launchd plist into ~/Library/LaunchAgents/
#    Linux: append the crontab snippet via `crontab -e`

# 6. Verify everything wires up
uv run python -m schwab_marketdata_mcp.health   # exit 0 if healthy
uv run pytest --cov                              # ≥85% overall, 100% on critical modules

For full client registration walkthroughs (Cursor / Claude Code / VS Code / Claude Desktop), see [docs/REGISTER.md](docs/REGISTER.md).


Features

Authentication & token lifecycle

  • OAuth 2.0 authorization code flow with two ergonomic CLIs:

login_flow (auto-opens a browser, captures the redirect) and manual_flow (paste-the-URL, for headless / containerized environments).

  • Atomic refresh-token rotation — every refresh writes to a tempfile and

os.replaces the live token, guarded by an fcntl.flock so concurrent agents never race or corrupt the on-disk token.

  • Permission audit — refuses to load a token file that is group- or

world-readable; emits SchwabAuthError(reason="insecure_token_perms") with a copy-pasteable chmod 600 hint.

  • 7-day refresh window detection — translates Schwab's opaque

invalid_grant into SchwabAuthError(reason="refresh_token_expired") so agents can surface a "reconnect" UX instead of looping.

Reliability

  • Per-process rate limiter — token-bucket, default 120 req/min,

configurable via SCHWAB_RATE_LIMIT_PER_MIN.

  • Adaptive retry on 429 and 5xx (default: 2 retries with exponential

backoff and Retry-After parsing).

  • Pluggable cache — a backend-agnostic response + derived-analysis cache

short-circuits repeat reads of the 5 cacheable tools (get_quote, get_price_history, get_option_chain, search_instruments, get_instrument_by_cusip). Per-table TTLs (60 s quotes / 5 m option chains / 24 h instruments / 60 s recent price history) cut Schwab API pressure. Disabled by default (opt-in) — every call hits Schwab live. Enable with SCHWAB_CACHE_ENABLED=true (also accepts 1 / yes / on); force fresh reads via SCHWAB_CACHE_BYPASS=1. When disabled, cacheable tools report _cache_status: "disabled"; the response shape is unchanged.

⚠️ BREAKING (v0.5.0): the embedded DuckDB cache is removed in favour of a pluggable backend selected via SCHWAB_CACHE_BACKEND:

| Backend | Default | Dependency | Notes | | --- | --- | --- | --- | | memory | ✅ | none (stdlib) | In-process LRU + TTL, concurrency-safe, non-blocking, no files. Derived-analysis history (option_chain_snapshots / iv_history / candle OLAP) keeps no durable store — those degrade gracefully (get_iv_percentile returns a cache_disabled/empty payload, snapshot writes report _cached_rows: 0). | | clickhouse | — | pip install schwab-marketdata-mcp[clickhouse] + SCHWAB_CLICKHOUSE_URL | Durably persists the derived-analysis time series and serves the real ATM-IV / IV-percentile / candle-OLAP analytics. |

All 17 tools keep working out of the box on the default memory backend; only the IV-history-backed analytics require the ClickHouse extra to retain cross-session history.

  • Health probe (schwab_marketdata_mcp.health) returns distinct exit

codes for token age, missing/malformed token, and insecure permissions — ready for cron / launchd alerting.

  • Rotating file logs under ${XDG_STATE_HOME}/schwab-marketdata-mcp/logs/

(10 MB × 5), regardless of whether the MCP host honors a stderr field.

Security

  • stdio hardeningbootstrap_dotenv() runs before any print() could

fire, so .env loading never corrupts the JSON-RPC stream.

  • Path-injection preventionSCHWAB_TOKEN_PATH env var is intentionally

unsupported; use the --config-dir CLI flag instead.

  • Pre-commit hardeninggitleaks, detect-secrets, ruff, mypy,

and markdownlint run on every commit; .secrets.baseline is committed.

  • Non-redistributable data guardrail — the companion workflows skill

refuses to write Schwab data into a public repo (it calls gh repo view --json isPrivate first).

Tooling surface — 17 MCP tools

At-a-glance map of name → endpoint:

| # | Tool | Endpoint | | -- | ----------------------------- | ------------------------------------------ | | 1 | get_quote | GET /{symbol_id}/quotes | | 2 | get_quotes | GET /quotes | | 3 | get_price_history | GET /pricehistory | | 4 | get_option_chain | GET /chains | | 5 | get_option_expiration_chain | GET /expirationchain | | 6 | get_option_greeks_summary | derived — net Greeks from GET /chains (no ClickHouse needed) | | 7 | get_market_hours | GET /markets | | 8 | get_market_hour_single | GET /markets/{market_id} | | 9 | get_movers | GET /movers/{symbol_id} | | 10 | search_instruments | GET /instruments | | 11 | get_instrument_by_cusip | GET /instruments/{cusip_id} | | 12 | health_check | local — token age + cache health | | 13 | get_server_info | local — versions + supported tool list | | 14 | get_cache_stats | local — cache backend (memory/clickhouse) + live entry count | | 15 | get_iv_percentile | local — ATM IV percentile rank from cached iv_history (refresh=True pulls fresh chain) | | 16 | get_iv_surface | local — ATM IV surface across 30d/60d/90d buckets from cached iv_history | | 17 | get_streaming_snapshot 🧪 | Streamer WebSocket — bounded snapshot |

Detailed per-tool reference is below.

Business tools (10) — Schwab Market Data Production endpoints
get_quote — single-symbol real-time quote
  • When: fetch the latest bid / ask / last for one symbol; quick P&L

read or one-off decision; not a batch.

  • Input: symbol (str — stock, ETF, index $XYZ, or OSI option),

fields? (list of field groups, e.g. ["quote", "fundamental"]).

  • Returns:

{: {quote: {bidPrice, askPrice, lastPrice, totalVolume, ...}, fundamental: {...}, reference: {...}}}

  • Example: {"symbol": "VOO"}
get_quotes — batch quote for ≤ 50 symbols
  • When: watchlist refresh, portfolio-wide scan, ETF / stock

comparison; one HTTP round-trip instead of N.

  • Input: symbols (list[str], len ≤ 50), fields?,

indicative? (bool — include indicative quotes for indices).

  • Returns: {: {quote: {...}, fundamental: {...}}, : {...}, ...}
  • Example: {"symbols": ["VOO", "QQQ", "SPY"]}
get_price_history — OHLC candles (minute → month)
  • When: technical analysis, backtests, charting, computing SMA / RSI

/ ATR or any windowed indicator.

  • Input: symbol, period_type (DAY / MONTH / YEAR / YTD),

period, frequency_type (MINUTE / DAILY / WEEKLY / MONTHLY), frequency. Pydantic pre-validates the legal cartesian product.

  • Returns: `{candles: [{open, high, low, close, volume, datetime}, ...],

symbol, empty}`

  • Example:

{"symbol": "VOO", "period_type": "DAY", "period": "FIVE_DAYS", "frequency_type": "MINUTE", "frequency": "EVERY_FIVE_MINUTES"}

get_option_chain — option chain snapshot with Greeks
  • When: option research, IV-rank analysis, covered-call /

cash-secured-put strike selection, vertical-spread modeling.

  • Input: symbol, plus a dozen optional filters —

contract_type? (CALL / PUT / ALL), strike_count?, from_date?, to_date?, strategy?, range?, volatility?, interest_rate?, days_to_expiration?

  • Returns:

{status, callExpDateMap: {...}, putExpDateMap: {...}, underlying: {...}, numberOfContracts}

  • Example: {"symbol": "VOO", "strike_count": 3, "contract_type": "ALL"}
get_option_expiration_chain — list of available expirations
  • When: pre-flight before get_option_chain; check whether weekly /

monthly / LEAPS expiries exist for an underlying.

  • Input: symbol.
  • Returns:

{expirationList: [{expirationDate, daysToExpiration, expirationType, settlementType}, ...]}

  • Example: {"symbol": "VOO"}
get_option_greeks_summary — net Greeks aggregation (no ClickHouse needed)
  • When: read net dealer/book positioning at a glance — aggregate

delta / gamma / theta / vega / rho across the live chain instead of eyeballing hundreds of contracts; split by call/put and by expiry; isolate a single expiration.

  • Works without ClickHouse: the Greeks are computed live from the

freshly-fetched chain (the data is already in get_option_chain), so this tool is useful on the default memory backend.

  • Input: underlying, expiry? (YYYY-MM-DD — restrict to one

expiration), weighting? (open_interest default / equal). open_interest weights each Greek by open interest and falls back to equal weighting (with a warning) when no contract reports OI.

  • Returns:

{underlying, expiry_filter, weighting, requested_weighting, contract_count, net: {delta, gamma, theta, vega, rho}, by_side: {CALL: {...}, PUT: {...}}, by_expiry: {: {...}}, warning}

  • Example: {"underlying": "AAPL", "weighting": "open_interest"}
get_market_hours — multi-market session hours
  • When: pre-market / after-hours dashboards; decide whether a market

is open before kicking off a scan; cross-market view.

  • Input: markets (list[str] — any of EQUITY / OPTION / BOND /

FUTURE / FOREX), date? (YYYY-MM-DD, defaults to today).

  • Returns:

{: {: {date, marketType, isOpen, sessionHours: {...}}}}

  • Example: {"markets": ["EQUITY", "OPTION"]}
get_market_hour_single — single-market session hours
  • When: same intent as get_market_hours but only one market —

smaller payload, path-param flavor.

  • Input: market_id (EQUITY / OPTION / BOND / FUTURE /

FOREX), date?.

  • Returns: same shape as get_market_hours, scoped to a single market.
  • Example: {"market_id": "EQUITY"}
get_movers — top movers for an index
  • When: intraday / end-of-day market-tone read; spot unusual movers

in $SPX / $DJI / $COMPX / NYSE / NASDAQ.

  • Input: index ($SPX / $DJI / $COMPX / NYSE / NASDAQ /

OTCBB / INDEX_ALL / EQUITY_ALL / OPTION_ALL / OPTION_PUT / OPTION_CALL), sort?, frequency?.

  • Returns:

{screeners: [{symbol, description, lastPrice, netChange, netPercentChange, volume, ...}, ...]}

  • Example: {"index": "$SPX"}
search_instruments — instrument search / fundamentals
  • When: resolve user-typed strings ("AAPL", "Apple") to canonical

instruments; bulk-validate tickers; pull fundamental block (PE, market cap, dividend yield, …).

  • Input: symbols (list[str]), projection

(SYMBOL_SEARCH / SYMBOL_REGEX / DESC_SEARCH / DESC_REGEX / SEARCH / FUNDAMENTAL).

  • Returns:

{instruments: [{symbol, description, exchange, assetType, fundamental?: {...}}, ...]}

  • Example: {"symbols": ["VOO"], "projection": "FUNDAMENTAL"}
get_instrument_by_cusip — reverse lookup by CUSIP
  • When: a brokerage statement / SEC filing gave you a 9-character

CUSIP and you need the matching ticker / description.

  • Input: cusip (str, 9 chars, must match ^[A-Z0-9]{9}$).
  • Returns: a single instrument dict

{symbol, description, exchange, assetType, ...}.

  • Example: {"cusip": "922908363"} (VOO)
Meta tools (2) — local, offline-safe, no Schwab API call
health_check — token + rate-limit + recent-error self-check
  • When: first call after MCP startup; before deciding "do we need to

re-auth"; periodic cron / launchd probe (every 4 h is plenty).

  • Input: none.
  • Returns:

{server_version, token_state (VALID | MISSING | INSECURE_PERMS | MALFORMED), token_age_days, token_expires_in_days, last_request_status, rate_limit_remaining_per_min, recent_error_count_24h, platform_supported}

  • Example: {}
get_server_info — version handshake + capability discovery
  • When: skill activation (validates compatible_mcp_version); first

time registering on a new client; debugging / bug reports.

  • Input: none.
  • Returns:

`{serverversion, mcpsdkversion, schwabpy_ve

Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.