schwab-mcp
Schwab MCP on Cloudflare Workers
English | 简体中文
Disclaimer
This project is for educational, research, and experimental automation purposes only. It does NOT constitute financial advice or investment recommendations. Trading securities and options carries substantial risk of loss. Authors and contributors assume no liability for any trading actions, losses, API errors, network latency, or LLM hallucinations.
A production-grade, single-user remote Model Context Protocol (MCP) server self-hosted on your personal Cloudflare Workers (100% Free-Tier friendly). It exposes Charles Schwab market data and account services to LLM clients (Cursor, Claude Desktop, VS Code) via Streamable HTTP (/mcp) with strict zero-trust security and anti-hallucination guardrails.
⚡ One-Line Quickstart with AI Agents
Got an AI coding assistant (Claude Code, Cursor Composer, Windsurf, or Codex)? Clone this repository and give your agent this single prompt:
💬 "Read
setup.mdand guide me step-by-step through configuring and deploying this Schwab MCP server to my Cloudflare account."
Your agent will automatically inspect prerequisites, provision Cloudflare KV & Secrets, validate code, and deploy to the edge!
Related MCP server: Remote MCP Server on Cloudflare
Key Highlights
100% Self-Hosted & Zero Maintenance: Deployed directly into your own Cloudflare account. Zero VPS, paid servers, or homelab hardware required—runs comfortably within Cloudflare's generous free tier with absolute data and credential sovereignty (no third-party proxy or SaaS middleman).
Zero-Trust Access: OAuth 2.1 dynamic client registration gated by an immutable GitHub Numeric User ID whitelist.
Granular Client Scopes: Each MCP client independently chooses
read_only(default, trading tools hidden) orfull_access.Physical Credential Isolation: Schwab tokens, state machines, and audit logs reside strictly within an SQLite-backed Durable Object (
SchwabVault). Secrets never leak into KV or logs.Anti-Hallucination Guardrails:
Global killswitch (
ENABLE_TRADING) defaults tofalse.Mandatory two-phase confirmation with verbatim phrase repetition for immediate and modified orders.
Optional ephemeral session direct trading (30-min idle timeout, 8-hour cap, single-reveal token).
Deterministic delayed conditional orders executed via Durable Object Alarms.
Zero blind write retries on network failures (
EXECUTION_UNCERTAIN).
Why Cloudflare Workers? (Comparison)
Feature | Typical Local MCP (Python/stdio) | schwab-mcp (This Project) |
Hosting & Cost | Local PC must stay awake, or paid VPS ($5–$20/mo) | Self-hosted on personal Cloudflare account ($0/mo, 100% Free Tier friendly) |
Data Sovereignty | Plaintext files on local disk | Private & self-hosted: you own your credentials in DO SQLite, zero 3rd-party SaaS |
Runtime & Deployment | Local machine process (dies on sleep/shutdown) | Cloudflare Workers Edge (24/7 Global Serverless, zero maintenance) |
Multi-Client Access | Single desktop only | Remote Streamable HTTP ( |
Credential Storage | Plaintext files on local disk | Isolated SQLite inside Durable Object ( |
Client Permissions | All tools exposed unconditionally | Dynamic OAuth Scope ( |
Trading Safety | None or weak prompt-based hints | Deployment killswitch ( |
Off-hours / Conditional Orders | Impossible if computer sleeps | Durable Object Alarms trigger deterministic conditional orders in the cloud |
Network Failure Safety | Risk of double-execution on blind retry |
|
Architecture
flowchart TD
subgraph ClientSide ["Client"]
Client["MCP Client (Cursor / Claude Desktop / Codex)"]
end
subgraph CloudflareEdge ["Cloudflare Workers Edge"]
Router["Worker Router (/mcp, /authorize, /auth/*)"]
OAuthProvider["@cloudflare/workers-oauth-provider"]
KV[("OAUTH_KV (Client & Grant Meta)")]
subgraph StorageDO ["SchwabVault Durable Object"]
DO["Durable Object Runtime"]
SQLite[("DO SQLite Storage\n- Schwab Tokens & Expiry\n- Proposals & Direct Session\n- Delayed Tasks\n- Redacted Audit Logs")]
Alarm["DO Alarm Scheduler\n(Auto-refresh / Delayed Tasks)"]
end
end
subgraph Upstream ["External Services"]
GitHub["GitHub OAuth (User ID Whitelist)"]
SchwabAPI["Charles Schwab REST APIs\n(Market Data & Trader)"]
end
Client -->|Streamable HTTP POST /mcp| Router
Router -->|Verify Access Token| OAuthProvider
OAuthProvider <-->|Persist Client / Grant| KV
Router -->|Authorized Tool Calls| DO
DO -->|Read GET / Controlled Write| SchwabAPI
Alarm -->|Trigger on Conditions| DO
User["Owner (Browser)"] -->|Authorize Client /authorize| Router
Router -->|Verify Identity| GitHub
User -->|Connect Schwab /auth/schwab/connect| Router
Router -->|Exchange Schwab Code| SchwabAPI
Router -->|Persist Tokens| DOExample Agent Workflows
1. Portfolio Analysis & Option Chain (Read-Only)
User: "What's my current position in NVDA, and what does the next monthly 140 CALL option look like?"
Agent: (Callsget_accountandget_option_chain)
"You hold 100 shares of NVDA at an average price of $118.40 (unrealized P&L: +18.2%). The 30-day $140 CALL is currently trading at $4.20 bid / $4.35 ask, with Delta 0.38 and IV of 42.1%."
2. Guarded Order Execution (Two-Phase Confirmation)
User: "Buy 10 shares of AAPL with a limit order at $220.00."
Agent: (Callsprepare_tradewith actionPLACEto freeze order snapshot)
"I have prepared and frozen the order snapshot:
- Action: BUY 10 AAPL @ Limit $220.00 (Day)
- Account:...8a9f
- Proposal ID:prop_01JABC...(Valid for 5 minutes)
⚠️ To confirm and execute, please reply verbatim with:CONFIRM BUY 10 AAPL LIMIT 220.00 HASH 8a9f"User:
CONFIRM BUY 10 AAPL LIMIT 220.00 HASH 8a9f
Agent: (Callsconfirm_tradeconsuming one-time token with verbatim phrase)
"Order successfully placed with Charles Schwab. Order ID:#10098234."
MCP Tools Catalog
The server registers 24 MCP tools. Schema validation strictly follows Zod definitions in src/schemas.ts.
Read-Only & Diagnostics (15 Tools)
Available to all authenticated clients. Operations use HTTP GET:
Tool | Description | Key Inputs |
| Check Schwab OAuth connection and expiration | None |
| Check tool registration, killswitch, and active trading status | None (always visible) |
| Fetch real-time market quotes |
|
| Historical OHLCV candle bars |
|
| Option chains, Greeks, and quotes |
|
| Expiration dates for option underlying |
|
| Market trading schedules |
|
| Market gainers/losers/actives |
|
| Search tickers or fundamental definitions |
|
| List account hash identifiers (real numbers stripped) | None |
| Balances and positions for an account hash |
|
| Execution history across accounts |
|
| Execution details for a specific order |
|
| Account cash flow and trade transactions |
|
| User profile context and trade permissions | None |
Trading Control Tools (9 Tools)
Only registered for clients grantedfull_access during OAuth authorization. Executing trades additionally requires ENABLE_TRADING=true. Cancellation remains available even when trading is disabled to de-risk positions.
Tool | Description | Key Inputs |
| Read-only check for deployment killswitch status | None (switch cannot be toggled via MCP) |
| Freeze order snapshot and issue 5-minute single-use token |
|
| Consume confirmation token with verbatim text playback |
|
| Enable direct order placement for the current session |
|
| Direct order submission bypassing per-order confirmation |
|
| Invalidate active direct trading capability immediately |
|
| Cancel working market order (no confirmation needed) |
|
| List active and historical conditional orders |
|
| Cancel an active conditional task immediately |
|
Safety & Security Guardrails
Deployment Killswitch (
ENABLE_TRADING):Configured in
wrangler.jsonc(default:"false").When
false, order preparations, confirmations, and session capability grants fail closed. Working orders can still be cancelled.
Anti-Hallucination Two-Phase Confirmation:
prepare_tradegenerates an immutable snapshot and a random confirmation sentence.The user must explicitly repeat the confirmation sentence before
confirm_tradeconsumes the token and executes the order.
Session Direct Trading Mode:
Limited strictly to
IMMEDIATE+PLACE(order modifications still require confirmation).30-minute idle expiry, refreshed on use; 8-hour hard lifetime.
Atomic idempotent claim using client-provided
executionKey.
Deterministic Delayed Orders:
AST-based rules combining
TIMEandQUOTEthresholds. Monitored by Durable Object Alarms.
No Blind Retries on Write (
EXECUTION_UNCERTAIN):Network errors or upstream 5xx on write requests mark the order as
EXECUTION_UNCERTAIN. Blind retries are blocked; inspect status manually withget_orders.
Payload Sanitization:
Max 64 KiB UTF-8 JSON, max depth of 16, max 2048 nodes. Real account numbers and sensitive tokens are strictly scrubbed.
Quickstart
Prerequisites
Node.js: >= 20.0.0 (LTS recommended)
Cloudflare Account: Workers & Durable Objects enabled
GitHub Account: OAuth App created for owner authentication
Charles Schwab Developer Account: Approved developer app with Trader & Market Data APIs
Local Setup
Install dependencies:
npm installConfigure local development variables:
cp .dev.vars.example .dev.varsFill in
.dev.vars(ignored by git):GITHUB_CLIENT_SECRET="your_github_client_secret" SCHWAB_CLIENT_ID="your_schwab_app_key" SCHWAB_CLIENT_SECRET="your_schwab_app_secret"Update
wrangler.jsonc:Set
GITHUB_CLIENT_IDandALLOWED_GITHUB_USER_ID(find viacurl -s https://api.github.com/users/<username> | grep '"id":').Set
SCHWAB_REDIRECT_URItohttps://<worker-name>.<subdomain>.workers.dev/auth/schwab/callback.Keep
ENABLE_TRADING="false"during development.
Run automated validation:
npm run validate(Runs typecheck, lint, and Vitest suite under simulated Workers DO environments).
Production Deployment
Step 1: Create KV & Set Secrets
# 1. Create KV namespace for OAuth client/token persistence
npx wrangler kv namespace create OAUTH_KV
# Put the returned id into wrangler.jsonc under kv_namespaces
# 2. Upload secrets interactively
npx wrangler secret put GITHUB_CLIENT_SECRET
npx wrangler secret put SCHWAB_CLIENT_ID
npx wrangler secret put SCHWAB_CLIENT_SECRETStep 2: Deploy Worker
npm run deployMCP authorization usesAUTHORIZATION_VERSION = "client-permissions-v1". Updating this version invalidates active grants, requiring clients to re-authenticate and pick permissions. Schwab account bindings and pending delayed trades remain intact.
The S256 PKCE update keeps this authorization version unchanged: existing valid access and refresh tokens retain their permissions, including grants issued without PKCE. Authorization codes issued before this update must restart authorization; pending consent or GitHub callback flows without valid S256 parameters are also rejected. Reauthorizing the same client still revokes its previous grants before issuing a replacement.
Step 3: CI/CD Pipeline (GitHub Actions)
The repository includes .github/workflows/deploy.yml for automated builds on main:
Repository Secrets:
CLOUDFLARE_ACCOUNT_ID,CLOUDFLARE_API_TOKENRepository Variables:
PROD_OAUTH_KV_ID,PROD_ALLOWED_GITHUB_USER_ID,PROD_GITHUB_CLIENT_ID,PROD_SCHWAB_REDIRECT_URI,PROD_ENABLE_TRADING(falseby default)
Run npm run configure:deploy in CI to generate the deployment wrangler.jsonc. Never put Schwab or GitHub client secrets in GitHub Actions!
Step 4: Authorize Charles Schwab
Visit in your browser:
https://<your-worker-domain>/auth/schwab/connectAuthenticate with GitHub (must match
ALLOWED_GITHUB_USER_ID).Log in to Schwab and approve account access.
Tokens are securely encrypted into
SchwabVaultDurable Object.
Client Configuration
MCP Endpoint: https://<your-worker-domain>/mcp
Every new MCP OAuth authorization requires RFC 7636 S256 PKCE. Server metadata advertises only S256; clients that lack S256 support must upgrade. Send exactly one code_challenge (43 unpadded base64url characters) and one code_challenge_method=S256 in the authorization request. Keep the independently generated random verifier on the client and send it as code_verifier only when exchanging the authorization code. Missing, duplicate, or invalid challenge/method parameters return HTTP 400 invalid_request before starting consent. Missing or incorrect verifiers fail token exchange through the OAuth provider.
Local protocol tests cover dynamic registration, browser consent, simulated GitHub owner verification, code exchange, refresh, and MCP calls for both permission levels. These tests do not establish compatibility with real client applications; reconnect and verify each client after deployment.
Claude Desktop
Add to claude_desktop_config.json:
{
"mcpServers": {
"schwab": {
"url": "https://<your-worker-domain>/mcp"
}
}
}Cursor / VS Code / Codex
Configure as a remote MCP server:
Type:
Streamable HTTPURL:
https://<your-worker-domain>/mcpOn first connection, complete the browser OAuth flow, select Read only or Full access, and verify via GitHub.
Error Codes & Operations
Code | Cause | Action |
| Schwab account not linked | Visit |
| Refresh token expired (7 days inactivity) | Visit |
| Payload failed Zod validation | Review input against |
| Schwab API rate limit reached (HTTP 429) | Back off following |
| Upstream business rejection | Check ticker, order status, or market hours |
| Schwab gateway or Durable Object error | Retry request (read requests retry once automatically) |
| Confirmation token mismatch or incorrect phrase | Re-run confirmation with exact text |
| Token exceeded 5-minute validity | Call |
| Proposal already consumed or task cancelled | Refresh state with |
| Session expired (30m idle / 8h max) | Call |
| Network failure during order submission | Do not blindly retry. Verify via |
Real-Time Log Monitoring
Stream sanitized structured JSON logs via Cloudflare Workers Observability:
npx wrangler tail schwab-mcp --format jsonAll headers, credentials, OAuth tokens, and account numbers are automatically redacted before logging.
License
Licensed under the Apache License, Version 2.0.
This server cannot be deployed
Maintenance
Related MCP Connectors
Multi-tenant FastMCP server for Charles Schwab brokerage data, monetized via DPYC Tollbooth
Production MCP server for US equity and options intelligence: real-time IV radar, Monte Carlo simulation, options pressure, strategy backtesting, AI prediction, pre-trade risk analysis, and automated stock research reports.
Hosted remote MCP server for YNAB on Cloudflare Workers with OAuth
Cloudflare Workers MCP server: ai-guardrails
Related MCP Servers
- FlicenseNot gradedqualityFmaintenanceA Model Context Protocol server that enables AI assistants like Claude to securely interact with Charles Schwab accounts and market data through the official Schwab API.81-
- FlicenseNot gradedqualityCmaintenanceA Model Context Protocol server implementation designed to run on Cloudflare Workers with integrated OAuth authentication. It enables hosting and securely accessing MCP tools remotely via SSE transport from clients like Claude Desktop.-
- AlicenseNot gradedqualityDmaintenanceA read-only Model Context Protocol server that connects your Schwab brokerage account to LLM applications for portfolio monitoring and market data retrieval.MIT
- FlicenseNot gradedqualityBmaintenanceA remote MCP server for market data that provides stock quotes, price history, option chains, finance news, and market snapshots, deployable as a Cloudflare Worker.62 npm-