Skip to main content
Glama
flamevip

Schwab MCP Server

by flamevip

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_presentorder_visible=truesubmitted_order_matches=trueprovider_order_status=closedclosed 映射 REJECTED/CANCELED/REPLACED/EXPIRED,具体状态、已成交数量及持仓影响仍未确认;不能宣称零成交、没有部分成交或没有后继替换订单。当次认证成功也不证明当前 Token 仍有效。

DOC1 复核时源码包含四个独立入口(本次本地查询新增第五入口见下文):src/entrypoints/baseline.tsstaging-preview.tsstaging-trading.tsstaging-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.jsonw4c-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 客户端 → WorkerAuthorization: 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_orderplace_previewed_ordercancel_orderreplace_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 + Zod

  • D1 + 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 dev

pnpm 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.jsoncsecrets.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 加密存储,日志严格脱敏。

  • 不自动重试具有不确定执行结果的金融写请求。

  • 第一版先证明真实纵向链路,再扩展工具数量。

验收路径

第一个可交付版本应完成:

  1. /mcp 可以被 MCP Inspector 发现。

  2. 缺失或错误 Bearer Token 时 /mcp 返回 401,正确 Token 可以调用 Tool。

  3. health_check 返回有效结构化结果。

  4. 用户可以完成 Schwab OAuth。

  5. Refresh Token 加密保存,Access Token 可自动刷新。

  6. get_quotes 能查询真实行情。

  7. Claude Code 能根据自然语言正确调用 get_quotes

  8. 延迟行情、认证失败、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_TOKENSCHWAB_MCP_STAGING_TOKENSCHWAB_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 开发资料快照:

正式开发包包含 docs/vendor/schwab/ 下的接口契约快照及校验和。更新上游快照时必须 记录来源、日期与新校验和,不要让构建依赖聊天附件或临时下载路径。