Schwab MCP Server
Schwab MCP Server
一个面向 ChatGPT、Claude 和其他 MCP 客户端的 Schwab Trader API 适配服务,计划 直接运行在 Cloudflare Workers 上。
项目已经完成单用户、只读、REST 优先远程 MCP 的 staging 纵向链路,并在独立交易预览
门禁下完成 W2B migration、W2D preview-only 部署和 W2E 一次真实 preview smoke。Phase
W4A/W4B 曾构建并短暂验证 Telegram Reject 链。W4C-R2 的 I0、I1、I2、I2-S0、I3、I4 已依据各自历史 provenance/receipt 完成;本次 DOC1 只复核本地证据,没有新的远程核验。E1 在 I3 前的“unresolved possible-order”停止点已 superseded:I3 返回 confirmed_present、order_visible=true、submitted_order_matches=true、provider_order_status=closed。closed 映射 REJECTED/CANCELED/REPLACED/EXPIRED,具体状态、已成交数量及持仓影响仍未确认;不能宣称零成交、没有部分成交或没有后继替换订单。当次认证成功也不证明当前 Token 仍有效。
DOC1 复核时源码包含四个独立入口(本次本地查询新增第五入口见下文):src/entrypoints/baseline.ts、staging-preview.ts、staging-trading.ts、staging-reconciliation.ts。本地 wrangler.jsonc 复核为 local false/false/false、staging true/true/true、production false/false/false(preview/Telegram/execution),canonical main 仍为 baseline;本地配置不是远端流量状态。I4 最后记录为旧 preview-only Version 5e5d…5e33 100%,trading 21f8…cb18 和 reconciliation 7c45…150e 均无流量,reconciliation/execution surface 关闭;Webhook 关闭继承 H0 的历史删除核验,I4 未重新查询 Telegram。migration 0012 来自 M1 receipt。不得由这些远端记录推断本地 gate 已关闭。
完成依据与独立 source identity 见 TASKS 的 DOC1 证据索引,I3/I4 分别见 .wrangler/release-state/w4c-r2-staging-reconciliation-result.json 与 w4c-r2-staging-reconciliation-restore.json。旧“I0 尚未冻结、I1–I4 未执行”描述已 superseded;reconciliation 曾临时部署并调用,I4 后不再承载流量。I3 对账完成、I4 恢复完成不等于 E1 原验收全部通过、整个交易项目完成或获准重新开放交易;进一步交易测试仍禁止。
旧 preview 恢复目标的身份与流量有历史证据,但 P1 新 preview artifact 的构建期隔离证明不绑定该旧 Version;尚缺旧 Version 自身的 source/build/能力排除完整来源链。这是来源证明缺口,不是已发现交易入口暴露;不能把 P1 证明套用到旧 Version,也不修改历史 receipt“修复”缺口。历史 source-freeze 保持不变,未来文档 commit 引用相应历史源码 commit 即可,不需要重新冻结源码或重做部署。
这里有两层完全独立的认证:
MCP 客户端 → Worker:Authorization: Bearer <MCP_BEARER_TOKEN>。Worker → Schwab:Schwab Authorization Code、Access Token 和 Refresh Token。
固定 Bearer Token 只用于保护这个单用户 MCP,不能替代 Schwab OAuth,也不能复用 Schwab Access Token。
浏览器不会可靠携带 MCP Bearer Header,因此 Schwab 首次连接采用一次性 URL:客户端
先用 Bearer 调用 POST /schwab/connect-ticket,服务返回短期 connect URL;浏览器打开
该 URL 后再进入 Schwab OAuth。Worker 使用短期 HttpOnly 流程 Cookie 绑定回跳,Bearer
Token 本身永远不进入 URL。附带的 Schwab 文档没有承诺回传标准 state,因此实现不能
只依赖 state 参数;Authorization Code 交换同时使用由 flow Cookie nonce 确定性派生、不会写入
D1 的 PKCE S256 verifier。
T7.6-S-MERGE1 本地整合进度(2026-09-08)
发布方案已改为先整合七 Tool,再发布验收。新入口 src/entrypoints/staging-preview-orders.ts 复用 preview 的 Bearer/Host/CORS、安全响应头及 OAuth HTTP/Token DO,追加单账户 list_orders / get_order。目标顺序为 health、preview、quotes、market hours、price history、list、get;preview 允许创建 D1 预览,list/get 不写交易业务状态,既有 OAuth 维护保留。不接 Telegram、审批、placement、撤改或事故 reconciliation。
本地配置已对齐 preview/Telegram/execution:local false/false/false、staging true/false/false、production false/false/false;canonical main 保持 baseline,资源身份未改,新入口不要求 Telegram Secret。本文下方 DOC1/C1 的三 gate=true 说明仅属历史状态,不再代表当前本地配置。远端未做任何 Secret 操作。
七 Tool 仅本地整合完成,具备本阶段源码冻结条件,但尚未冻结。 本轮隔离全套 synthetic 测试共 66 文件/1739 tests,最终实际七 Tool artifact runtime 97 checks 通过,类型 freshness/tsc、lint/format 和必要安全边界均通过;权限阻断和早期失败记录保留,不算作成功。F1/U1 保持历史完成;U1 六 Tool 候选已上传但未部署、零流量,现不采用,历史证据保留。远端仅按 U1 最后记录为旧 preview-only 5e5d…5e33 100%,不是本轮 fresh 查询。七 Tool 新版本未冻结、上传或部署;后续真实验收仅查询已有订单,不创建订单,原事故具体终态与成交/持仓影响仍未解决。详见 MERGE1 台账。
当前范围
MVP Tool:
health_check:检查 MCP 服务是否可用。get_quotes:查询一个或多个标的的精简行情。get_market_hours:查询指定市场和日期的开闭市信息。get_price_history:查询股票历史 K 线。preview_equity_order:默认隐藏;仅当服务端精确配置TRADING_PREVIEW_ENABLED=true时创建短期、需审批的股票订单预览并写入内部 D1;不发送审批、不调用 Schwab 下单端点。place_previewed_order:仅在 trading composition 且 preview、Telegram、execution 三个 gate 全部为true时注册;输入只有preview_id,只请求一次 Telegram 审批并返回approval_pending,Tool 调用期间不下单。Approve 才在同一 webhook 流中触发一次真实提交;Reject 永不提交。reconcile_equity_execution:只属于独立临时 reconciliation composition,固定事故订单的 exact-ID 只观察核对;不属于旧 preview-only Version 的 Tool 集合,也不是通用订单查询。
以上是不同源码入口的能力,不是同一活动部署的 Tool 清单。2026-09-07 明确授权 T7.6-S 单账户只读订单查询本地子阶段:新增独立 src/entrypoints/readonly-orders.ts,注册原四核心 Tool 加 list_orders / get_order,共六个只读 Tool;原四入口、canonical main 和交易 gate 均不变。列表要求明确录入时间范围(本地最多 365 天)、返回限制(默认 100、最多 500)及可选具体状态;详情仅接受账户/部署/用途绑定的 AES-GCM opaque order_ref。保留具体状态、原/已成交/剩余数量及已提供的成交明细,不补零;列表完整性保守未知,达到上限标记可能截断,空列表/404 不作为重下单依据。状态自由文本不透传。
本阶段复用唯一 OAuth 授权账户 resolver,账户隔离、脱敏和引用稳定性作为必要验收;完整 T7.4、T7.6 和 M7 仍未完成/全面解锁,不实现多账户、昵称或主账户切换。该新入口没有发布 wrapper,不开放 OAuth HTTP/Telegram 或交易状态写入,仅复用普通 Token DO 维护。接口、资源预算、int64/symbol 限制及本地测试证据见 T7.6-S。仅本地完成不代表上线;未发布,不恢复交易。
Phase W1.1 只增加预览和内部安全状态机基础。预览会修改内部持久状态,因此 annotation 为 readOnlyHint=false;它不会向 Schwab 提交订单,因此 destructiveHint=false,且每次生成新的 preview_id,所以 idempotentHint=false。W2A.3 已取代 W2A.1 的账户 Secret 方案:每个实际预览请求使用现有 OAuth Access Token 调用固定只读端点 GET /trader/v1/accounts/accountNumbers,且只有返回恰好一个有效、唯一授权账户映射时才继续;零账户、多账户、重复或 malformed 响应全部 fail closed,不选择第一项。accountNumber 立即丢弃,Schwab hashValue 只在当前请求内存中用于生成 domain-separated account_ref_hash,D1 仅保存后者。OAuth connected 状态与此后续 Trader API 账户发现彼此独立。
历史说明:W1/W2A 文档中“0010/0011 尚未远程 apply”“W2B 尚未授权”“staging 尚未执行真实 preview”的描述只记录当时状态,现已被 W2B、W2D、W2E 台账 supersede;“远程 staging migration 止于 0011、0012 尚未 apply”又已被 W4C-R2-M1 supersede,当前 receipt 证据精确到 0012。preview-only Version 已部署且 TRADING_PREVIEW_ENABLED=true;W2E 的一次真实 preview smoke 只创建了一条约 5 分钟 TTL 的内部预览,没有审批或订单提交。该历史事实不改变后续 I3 已确认订单存在且 closed、具体成交影响未知及继续禁止交易测试的边界;I3 前 E1 unresolved 的记录保留为 superseded 历史状态。
Phase W3B 增加本地、默认关闭的 Telegram 私聊审批 transport。Worker 只向一个固定 private chat 发送纯文本 Approve/Reject 消息;POST /telegram/webhook 只接受经独立 webhook Secret 认证的 callback_query,并固定绑定唯一 approver、private chat、message reference 和一次性 action token。D1 只保存这些标识的 domain-separated hash;Telegram payload 不能提供或覆盖订单、账户、environment、deployment 或 order hash。Bot Token 与 Webhook Secret 分离,禁止 getUpdates,不支持 group、channel、多 Provider、fallback 或审批绕过。
审批决定通过跨 preview/approval 表的条件更新原子竞争:Approve 只允许 approval_pending → approved,Reject 只允许 approval_pending → rejected;重投、重放、并发败者或绑定不匹配全部 fail closed。W3B 不注册或调用 place_order、place_previewed_order、cancel_order、replace_order,也没有可达的 Schwab placement adapter;approved 只表示审批记录成功。W4A 已在本地接通该闭环:place_previewed_order 只发起异步审批;Approve 在同一处理流中重新发现账户、重算 order hash、复核 expiry/context/version、原子 approved → placing 后自动提交服务端保存的精确订单,Reject 永不执行。placed 只表示订单已提交,不代表成交;placement_unknown 必须人工核对且永不自动重试。W4C-R1 的 preview-only desired config 是历史状态,现已被 C1 source profile supersede;活动 deployed profile 则由 containment 恢复为 preview-only 五 Tool、100% 流量,Webhook 已删除且 trading Version 无流量。不得把 source gate 与 deployed execution surface 混为一谈。
历史 W3A Discord REST/Interaction 原型已 superseded:它于 2026-09-03 仅在本地完成实现和测试,始终未配置、激活或部署,没有真实 Discord 请求或交易执行;其任务与测试证据保留在 docs/TASKS.md,活动代码、配置和路由不再存在。
完整产品能力
以下能力均已纳入路线图,但会按阶段启用。
能力域 | 计划功能 |
市场行情 | 单标的/批量报价、盘前盘后、延迟标记、股票/期权/期货/外汇/指数/基金 |
历史与日历 | 分钟/日/周/月 K 线、前收盘、市场开闭市与交易时段 |
标的发现 | Symbol/名称/正则/CUSIP 搜索、Reference、基本面和做空属性 |
市场扫描 | 指数与交易所 Movers、涨跌榜、成交量榜、实时 Screener |
期权 | 到期日、期权链、Greeks、IV、成交量、持仓量和策略链 |
账户组合 | 账户、余额、现金、购买力、保证金、持仓、成本和盈亏 |
订单与流水 | 订单列表/详情/状态、交易流水、股息、利息、ACH、汇款等 |
实时数据 | Level 1、Level 2 Book、分钟图、Screener、账户活动与成交事件 |
确定性分析 | 组合集中度、风险敞口、期权筛选、策略收益、市场摘要和交易日志 |
应用数据 | 应用自有 Watchlist、提醒规则、事件记录和用户风险策略 |
受控交易 | 草稿、预览、股票/期权下单、改单、撤单、OCO、Trigger、Bracket、多腿期权 |
UI 与运维 | 可选 MCP Apps/ChatGPT UI、密钥轮换、容量、恢复和发布治理 |
不由当前 Schwab API 直接支持的新闻、财报日历、研报、资金转账、税务报表和历史 Tick 数据不在核心范围;若要加入,需要独立数据源和新的产品评审。高频及无人值守自动交易 不作为目标。
完整需求见 PRD,架构见 ARCHITECTURE,实施顺序见 TASKS,环境资源、迁移和 人工发布流程见 DEPLOYMENT,Bearer 客户端接入与轮换见 MCP_CLIENTS。
技术栈
TypeScript + Cloudflare Workers
Stateless MCP Streamable HTTP
Cloudflare Agents SDK
createMcpHandler@modelcontextprotocol/server+ ZodD1 + SQLite-backed Durable Objects
Workers Secrets / Secrets Store + Web Crypto AES-256-GCM
Cache API、Queues;实时阶段可选独立 Stream Service
Vitest + Cloudflare Workers 测试运行时
Wrangler + pnpm
架构概览
flowchart TD
Client["Claude Code / MCP Client"] --> Auth["静态 Bearer 认证"]
Auth --> Worker["Cloudflare Worker /mcp"]
Worker --> Schwab["Schwab REST / OAuth"]
Worker --> TokenDO["单例 Token Durable Object"]
TokenDO --> D1["D1 加密状态与审计"]
Worker -. "仅 trading root + 三 gate;I4 后无流量" .-> Telegram["Telegram private Bot / webhook"]
TokenDO -. "后续实时阶段" .-> Stream["Schwab Streamer"]Worker 提供无状态 MCP 端点;Durable Object 仅承担该部署的 Token 刷新锁、短期 Access Token 缓存和未来的账户级顺序控制。D1 保存加密后的 Schwab 连接信息与审计 记录。
开发前置条件
Node.js 当前 LTS。
pnpm。
Cloudflare 账户与 Wrangler CLI。
已批准并可使用的 Schwab Developer App。
Schwab App 中配置的 HTTPS Callback URL。
用于 staging 的 Cloudflare Worker 子域名或自有域名。
不要把 Schwab Client Secret、Token 或真实账户号写入仓库、Issue、测试 fixture 或 聊天记录。
本地开发流程
pnpm install
cp .dev.vars.example .dev.vars
# 在 .dev.vars 中仅填写当前本地流程所需的合成/开发 Secret;不要使用真实凭据。
# W3B 的 Telegram gate=false;当前本地流程不需要或读取 Telegram Secret。
pnpm db:migrate:local
pnpm devpnpm db:migrate:local 始终使用 Wrangler --local D1,状态写入已忽略的 .wrangler/,
不会连接或修改远程数据库。当前 SCHWAB_DB 配置仅供本地开发和测试使用;staging 与
production 部署前必须分别创建 D1、配置各自真实 database_id 并单独评审迁移,不能共享
Token 数据。
另一个终端可以启动 MCP Inspector UI:
npx @modelcontextprotocol/inspector@latest连接 http://localhost:8787/mcp 时,在 Inspector 的 HTTP Headers 配置中添加:
Authorization: Bearer <与 .dev.vars 中相同的本地 Token>也可以用 Inspector CLI 验证 Tool 发现;Token 只从当前终端环境读取,不写入仓库配置:
npx @modelcontextprotocol/inspector@2.1.0 --cli \
--server-url http://localhost:8787/mcp \
--transport http \
--method tools/list \
--header "Authorization: Bearer ${SCHWAB_MCP_LOCAL_TOKEN}"当前仓库已提供 Worker 脚手架、质量检查脚本、安全忽略规则和 .dev.vars.example。
可运行 pnpm test:gitignore 验证本地敏感文件策略。不要把真实 Token 写入 Inspector
配置文件、Shell 历史或仓库。
使用 Claude Code 开始开发
在包含本 README 和 CLAUDE.md 的项目根目录运行:
git init
claude第一次会话输入:
请完整阅读 CLAUDE.md、README.md、docs/PRD.md、docs/ARCHITECTURE.md 和
docs/TASKS.md。严格按依赖顺序,本次只实施 T0.1。开始前复述范围、依赖和验收标准;
完成后运行验证命令,把证据写回 TASKS.md。不要实施 T0.2 或后续任务,不要部署生产。每次只解锁一个依赖已满足的任务。历史 bootstrap 阶段“执行 T0.1 前没有可启动 Worker”的 说明已经 supersede;当前已有 staging Worker,但任何新的 Telegram Secret、webhook 配置、deploy 或交易执行步骤仍必须按 Phase 门禁单独授权。
配置边界
Cloudflare 生成的 Env 只在 Worker 组合根和配置解析器中读取。每个已认证 /mcp
请求会把原始字符串配置校验并转换为冻结的 AppConfig;MCP 与后续业务模块只接收所需的
窄类型投影,不能任意读取原始 Env。Worker bindings(后续 D1、Durable Object)与文本
配置分开注入,不进入 AppConfig。
非敏感 vars 放 wrangler.jsonc:
APP_ENV # local | test | staging | production;test 只用于测试注入
DEPLOYMENT_ID # 环境唯一、持久的非敏感加密域标识
PUBLIC_BASE_URL # HTTPS origin,不含路径、查询或 fragment
SCHWAB_REDIRECT_URI # 必须等于 PUBLIC_BASE_URL + /schwab/callback
SCHWAB_API_BASE_URL # 固定为 https://api.schwabapi.com
SCHWAB_OAUTH_BASE_URL # 固定为 https://api.schwabapi.com/v1/oauth
MCP_ALLOWED_HOSTNAMES # 逗号分隔的自定义 Host;空字符串表示无额外 Host
TRADING_PREVIEW_ENABLED # 只有精确字符串 true 才启用预览;desired false / true / false
TELEGRAM_APPROVAL_ENABLED # 只有精确字符串 true 才启用 Telegram;desired false / true / false
TRADING_EXECUTION_ENABLED # 只有精确字符串 true 才启用执行闭环;desired false / true / false
TELEGRAM_PRIVATE_CHAT_ID # gate=true 时固定唯一 private chat;正 safe-integer 十进制字符串
TELEGRAM_APPROVER_USER_ID # gate=true 时固定唯一 approver;正 safe-integer 十进制字符串所有 URL 均强制 HTTPS。Schwab Base URL 通过服务端精确 allowlist 固定,Tool 输入不能提供 或覆盖 URL。production 会拒绝 localhost、IP、保留测试/示例域名和明显占位 Secret。
敏感配置放 .dev.vars 或 Cloudflare Secret:
SCHWAB_CLIENT_ID
SCHWAB_CLIENT_SECRET
MCP_BEARER_TOKEN
TOKEN_ENCRYPTION_KEY
TELEGRAM_BOT_TOKEN # staging source profile gate=true 时额外声明;值不进入文档
TELEGRAM_WEBHOOK_SECRET # 与 Bot Token 分离;当前 Webhook 已删除wrangler.jsonc 的 secrets.required 为 staging source profile 声明原有四个 Secret 加两个 Telegram Secret 名称;local/production 仍只声明四个。名称声明不表示活动 Version 可达这些能力;gate=false 时运行时不得读取 Telegram 配置。专用 staging Bot 和身份引导是历史准备,当前 containment 已删除 Webhook并让 trading Version 保持 0% 流量。DEPLOYMENT_ID 必须在三个环境中不同且保持稳定;它绑定 Refresh Token 密文的
AES-GCM AAD,已保存 Token 后修改会使旧密文无法解密。生产环境通过 Wrangler Secret 或
Secrets Store 配置;不需要额外交易账户 Secret,W2A.1 曾提出的两个账户 Secret 从未上传且已由 W2A.3 supersede,不提供 fallback、alias 或迁移。配置错误只标识变量名和
稳定原因码,不保留、记录或返回变量值。TOKEN_ENCRYPTION_KEY 使用带标准 padding 的
canonical Base64,解码后必须严格为 32 字节,再作为不可导出的 AES-256-GCM Key 导入;T2.1
只验证配置存在性和 production 占位值,严格解码与导入由 T3.2 适配器执行。浏览器 OAuth
要求 HTTPS Public Base URL 和 Callback;本地联调需使用 HTTPS 本地域名或 tunnel,不能把普通
HTTP dev 地址配置为 Callback。
远程 MCP 默认关闭通配 CORS;Claude Code、Codex 和 Responses API 都是非浏览器客户端, 不需要开放跨域。若未来 MCP Apps UI 确实需要浏览器 Origin,再按明确 allowlist 开放。
预期项目结构
.
├── CLAUDE.md
├── README.md
├── docs/
│ ├── PRD.md
│ ├── ARCHITECTURE.md
│ ├── TASKS.md
│ └── vendor/schwab/
├── migrations/
├── src/
│ ├── index.ts
│ ├── mcp/
│ ├── tools/
│ ├── schwab/
│ ├── domain/
│ ├── analytics/
│ ├── streaming/
│ ├── orders/
│ └── storage/
├── test/
├── package.json
├── tsconfig.json
└── wrangler.jsonc开发原则
一个 Tool 对应一个清晰的用户目标。
只返回模型真正需要的精简结构,不默认返回原始 Schwab JSON。
所有外部响应都先按
unknown处理,再用 Schema 校验。明确标识行情实时或延迟状态。
Token 加密存储,日志严格脱敏。
不自动重试具有不确定执行结果的金融写请求。
第一版先证明真实纵向链路,再扩展工具数量。
验收路径
第一个可交付版本应完成:
/mcp可以被 MCP Inspector 发现。缺失或错误 Bearer Token 时
/mcp返回401,正确 Token 可以调用 Tool。health_check返回有效结构化结果。用户可以完成 Schwab OAuth。
Refresh Token 加密保存,Access Token 可自动刷新。
get_quotes能查询真实行情。Claude Code 能根据自然语言正确调用
get_quotes。延迟行情、认证失败、Schwab 授权失败和上游故障都有明确且脱敏的响应。
分阶段路线
M0–M6 平台、Bearer 认证、Schwab OAuth、行情 MVP、Claude Code 接入
M7 标的搜索、Movers、账户、持仓、订单与流水只读
M8 期权链、期权筛选和确定性策略分析
M9 投资组合分析、Watchlist、提醒和市场摘要
M10 Schwab Streamer 实时服务
M11 订单预览与受控股票/期权交易
M12 可选 UI、生产强化与规模化所有路线功能都进入文档,但不是同时进入开发范围。每个里程碑必须通过自己的验收和 安全门禁后,Claude Code 才能开始下一个里程碑。
MCP 客户端配置
staging MCP URL 固定为:
https://schwab-mcp-staging.namework.workers.dev/mcp客户端分别使用 SCHWAB_MCP_LOCAL_TOKEN、SCHWAB_MCP_STAGING_TOKEN 和
SCHWAB_MCP_PRODUCTION_TOKEN;三个值必须独立,且不得写入仓库或共享 .mcp.json。Claude Code
配置只保存环境变量引用:
claude mcp add --transport http --scope local \
schwab-staging https://schwab-mcp-staging.namework.workers.dev/mcp \
--header 'Authorization: Bearer ${SCHWAB_MCP_STAGING_TOKEN}'Codex 使用 bearer_token_env_var,Inspector 使用 ad-hoc HTTP Header。安全输入、清除、三个客户端
的允许验证范围、配置删除命令和泄漏后轮换流程见
MCP_CLIENTS。production 仍必须使用独立 MCP_BEARER_TOKEN,实际上传与
production 客户端验证只在 T6.6 production 部署门禁中执行。T6.2 不测试 Responses API 或 ChatGPT
Web;更深入的 OpenAI 兼容性仍属于 T6.5,未经实测不宣称 ChatGPT Web 已兼容。
参考资料
需求与架构分析基于以下 Schwab 开发资料快照:
market-data.openapi.yaml:Market Data REST OpenAPI。trader.openapi.yaml:Account、Order、Transaction REST OpenAPI。market-data.md:Schwab Streamer API 文档。trader.md:OAuth 与订单示例文档。
正式开发包包含 docs/vendor/schwab/ 下的接口契约快照及校验和。更新上游快照时必须
记录来源、日期与新校验和,不要让构建依赖聊天附件或临时下载路径。