Schwab MCP
Servidor MCP de Charles Schwab de grado de producción y priorizando la seguridad, autoalojado en Cloudflare Workers. Cotizaciones en tiempo real, griegas de opciones, OAuth 2.1 de confianza cero y salvaguardas contra alucinaciones.
Documentación
Schwab MCP en Cloudflare Workers
Inglés | 简体中文
[!CAUTION] Aviso legal
Este proyecto es solo para fines educativos, de investigación y automatización experimental. NO constituye asesoramiento financiero ni recomendaciones de inversión. Operar valores y opciones conlleva un riesgo sustancial de pérdida. Los autores y colaboradores no asumen ninguna responsabilidad por acciones de trading, pérdidas, errores de API, latencia de red o alucinaciones de LLM.
Un servidor Model Context Protocol (MCP) remoto de producción, para un solo usuario, autoalojado en tus Cloudflare Workers personales (100% compatible con el plan gratuito). Expone los servicios de datos de mercado y cuentas de Charles Schwab a clientes LLM (Cursor, Claude Desktop, VS Code) mediante Streamable HTTP (/mcp) con estrictas medidas de seguridad zero-trust y salvaguardas contra alucinaciones.
⚡ Inicio rápido en una línea con agentes de IA
¿Tienes un asistente de codificación con IA (Claude Code, Cursor Composer, Windsurf o Codex)? Clona este repositorio y dale a tu agente este único prompt:
💬 "Lee
setup.mdy guíame paso a paso para configurar e implementar este servidor Schwab MCP en mi cuenta de Cloudflare."
¡Tu agente inspeccionará automáticamente los requisitos previos, aprovisionará Cloudflare KV y Secrets, validará el código y hará el despliegue en el edge!
Características principales
- 100% Autoalojado y cero mantenimiento: Desplegado directamente en tu propia cuenta de Cloudflare. Sin VPS, servidores de pago ni hardware de homelab: funciona cómodamente dentro del generoso plan gratuito de Cloudflare con soberanía absoluta de datos y credenciales (sin proxy de terceros ni intermediarios SaaS).
- Acceso Zero-Trust: Registro dinámico de clientes OAuth 2.1 controlado por una lista blanca inmutable de ID numérico de usuario de GitHub.
- Ámbitos de cliente granulares: Cada cliente MCP elige independientemente
read_only(predeterminado, herramientas de trading ocultas) ofull_access. - Aislamiento físico de credenciales: Los tokens de Schwab, las máquinas de estado y los registros de auditoría residen estrictamente dentro de un Durable Object respaldado por SQLite (
SchwabVault). Los secretos nunca se filtran a KV ni a registros. - Salvaguardas contra alucinaciones:
- Interruptor global de apagado (
ENABLE_TRADING) con valor predeterminadofalse. - Confirmación obligatoria en dos fases con repetición textual de la frase para órdenes inmediatas y modificadas.
- Trading directo de sesión efímero opcional (tiempo de espera de inactividad de 30 minutos, límite de 8 horas, token de revelación única).
- Órdenes condicionales diferidas deterministas ejecutadas mediante Durable Object Alarms.
- Cero reintentos ciegos en fallos de red (
EXECUTION_UNCERTAIN).
- Interruptor global de apagado (
¿Por qué Cloudflare Workers? (Comparación)
| Característica | MCP local típico (Python/stdio) | schwab-mcp (Este proyecto) |
|---|---|---|
| Alojamiento y coste | El PC local debe permanecer encendido, o VPS de pago ($5–$20/mes) | Autoalojado en tu cuenta personal de Cloudflare ($0/mes, 100% compatible con el plan gratuito) |
| Soberanía de datos | Archivos en texto plano en el disco local | Privado y autoalojado: tú posees tus credenciales en DO SQLite, cero SaaS de terceros |
| Ejecución y despliegue | Proceso en máquina local (muere al dormir/apagar) | Cloudflare Workers Edge (serverless global 24/7, cero mantenimiento) |
| Acceso multi-cliente | Solo escritorio único | Streamable HTTP remoto (/mcp) en Cursor, Claude, VS Code y móvil |
| Almacenamiento de credenciales | Archivos en texto plano en el disco local | SQLite aislado dentro de Durable Object (SchwabVault), cero fugas a KV/registros |
| Permisos de cliente | Todas las herramientas expuestas incondicionalmente | Ámbito OAuth dinámico (read_only vs full_access) por cliente |
| Seguridad de trading | Ninguna o sugerencias débiles basadas en prompts | Interruptor de apagado de despliegue (ENABLE_TRADING), repetición de frase en dos fases, capacidad de sesión |
| Órdenes fuera de horario / condicionales | Imposible si el ordenador duerme | Durable Object Alarms activan órdenes condicionales deterministas en la nube |
| Seguridad ante fallos de red | Riesgo de doble ejecución en reintento ciego | El estado EXECUTION_UNCERTAIN bloquea reenvíos ciegos |
Arquitectura
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| DO
Ejemplos de flujos de trabajo del agente
1. Análisis de cartera y cadena de opciones (solo lectura)
Usuario: "¿Cuál es mi posición actual en NVDA y cómo se ve la opción CALL de 140 del próximo mes?"
Agente: (Llama aget_accountyget_option_chain)
"Tienes 100 acciones de NVDA a un precio promedio de $118.40 (P&L no realizado: +18.2%). El CALL de $140 a 30 días se cotiza actualmente a $4.20 oferta / $4.35 demanda, con Delta 0.38 e IV de 42.1%."
2. Ejecución de órdenes protegida (confirmación en dos fases)
Usuario: "Compra 10 acciones de AAPL con una orden limitada a $220.00."
Agente: (Llama aprepare_tradecon acciónPLACEpara congelar la instantánea de la orden)
"He preparado y congelado la instantánea de la orden:
- Acción: COMPRAR 10 AAPL @ Límite $220.00 (Día)
- Cuenta:...8a9f
- ID de propuesta:prop_01JABC...(válido por 5 minutos)
⚠️ Para confirmar y ejecutar, responde textualmente con:
CONFIRM BUY 10 AAPL LIMIT 220.00 HASH 8a9f"Usuario:
CONFIRM BUY 10 AAPL LIMIT 220.00 HASH 8a9f
Agente: (Llama aconfirm_tradeconsumiendo el token de un solo uso con la frase textual)
"Orden colocada exitosamente en Charles Schwab. ID de orden:#10098234."
Catálogo de herramientas MCP
El servidor registra 24 herramientas MCP. La validación de esquemas sigue estrictamente las definiciones de Zod en src/schemas.ts.
Herramientas de solo lectura y diagnóstico (15 herramientas)
Disponibles para todos los clientes autenticados. Las operaciones usan HTTP GET:
| Herramienta | Descripción | Entradas clave |
|---|---|---|
schwab_connection_status | Verificar conexión OAuth de Schwab y expiración | Ninguna |
get_trading_diagnostics | Verificar registro de herramientas, interruptor de apagado y estado de trading activo | Ninguna (siempre visible) |
get_quotes | Obtener cotizaciones de mercado en tiempo real | symbols (matriz de 1–50), fields, indicative |
get_price_history | Velas OHLCV históricas | symbol, periodType, frequencyType, frequency, startDate, endDate |
get_option_chain | Cadenas de opciones, griegas y cotizaciones | symbol, contractType (CALL/PUT/ALL), strikeCount, strategy |
get_option_expiration_chain | Fechas de expiración para el subyacente de opciones | symbol |
get_market_hours | Horarios de negociación del mercado | markets (equity, option, bond, etc.), date (YYYY-MM-DD) |
get_movers | Ganadores/perdedores/activos del mercado | index ($DJI, $COMPX, $SPX), sort, frequency |
search_instruments | Buscar tickers o definiciones fundamentales | symbol, projection |
list_accounts | Listar identificadores hash de cuenta (números reales eliminados) | Ninguna |
get_account | Saldos y posiciones para un hash de cuenta | accountHash, includePositions |
get_orders | Historial de ejecuciones en todas las cuentas | fromEnteredTime, toEnteredTime (ISO-8601), status |
get_order | Detalles de ejecución para una orden específica | accountHash, orderId |
get_transactions | Flujo de caja de cuenta y transacciones de trading | accountHash, startDate, endDate (YYYY-MM-DD), types, symbol |
get_user_preferences | Contexto de perfil de usuario y permisos de trading | Ninguna |
Herramientas de control de trading (9 herramientas)
[!WARNING] Solo se registran para clientes a los que se les otorgó
full_accessdurante la autorización OAuth. Ejecutar operaciones además requiereENABLE_TRADING=true. La cancelación sigue disponible incluso cuando el trading está deshabilitado para reducir el riesgo de posiciones.
| Herramienta | Descripción | Entradas clave |
|---|---|---|
enable_trading | Verificación de solo lectura del estado del interruptor de apagado de despliegue | Ninguna (el interruptor no se puede alternar vía MCP) |
prepare_trade | Congelar instantánea de orden y emitir token de un solo uso de 5 minutos | accountHash, action (PLACE/REPLACE), executionMode, orderPayload, userInstruction |
confirm_trade | Consumir token de confirmación con reproducción textual | proposalId, confirmationToken, confirmationText |
enable_session_direct_trading | Habilitar colocación directa de órdenes para la sesión actual | userInstruction (devuelve sessionCapability de revelación única) |
place_order_direct | Envío directo de órdenes sin confirmación por orden | sessionCapability, executionKey (UUID), accountHash, orderPayload, userInstruction |
disable_session_direct_trading | Invalidar capacidad de trading directo activa inmediatamente | sessionCapability |
cancel_order | Cancelar orden de mercado en curso (sin confirmación necesaria) | accountHash, orderId, userInstruction |
list_delayed_trades | Listar órdenes condicionales activas e históricas | status (ACTIVE, EXECUTING, EXECUTED, etc.) |
cancel_delayed_trade | Cancelar tarea condicional activa inmediatamente | taskId, userInstruction |
Salvaguardas de seguridad y protección
- Interruptor de apagado de despliegue (
ENABLE_TRADING):- Configurado en
wrangler.jsonc(predeterminado:"false"). - Cuando
false, las preparaciones de órdenes, confirmaciones y concesiones de capacidad de sesión fallan de forma segura. Las órdenes en curso aún pueden cancelarse.
- Configurado en
- Confirmación en dos fases contra alucinaciones:
prepare_tradegenera una instantánea inmutable y una frase de confirmación aleatoria.- El usuario debe repetir explícitamente la frase de confirmación antes de que
confirm_tradeconsuma el token y ejecute la orden.
- Modo de trading directo de sesión:
- Limitado estrictamente a
IMMEDIATE+PLACE(las modificaciones de órdenes aún requieren confirmación). - Expiración por inactividad de 30 minutos, renovada con el uso; vida útil máxima de 8 horas.
- Reclamación atómica e idempotente usando
executionKeyproporcionado por el cliente.
- Limitado estrictamente a
- Órdenes diferidas deterministas:
- Reglas basadas en AST que combinan umbrales de
TIMEyQUOTE. Supervisadas por Durable Object Alarms.
- Reglas basadas en AST que combinan umbrales de
- Sin reintentos ciegos en escrituras (
EXECUTION_UNCERTAIN):- Errores de red o 5xx del proveedor en solicitudes de escritura marcan la orden como
EXECUTION_UNCERTAIN. Los reintentos ciegos están bloqueados; inspecciona el estado manualmente conget_orders.
- Errores de red o 5xx del proveedor en solicitudes de escritura marcan la orden como
- Saneamiento de cargas útiles:
- Máximo 64 KiB de JSON UTF-8, profundidad máxima de 16, máximo 2048 nodos. Los números de cuenta reales y tokens sensibles se eliminan estrictamente.
Inicio rápido
Requisitos previos
- Node.js: >= 20.0.0 (LTS recomendado)
- Cuenta de Cloudflare: Workers y Durable Objects habilitados
- Cuenta de GitHub: Aplicación OAuth creada para autenticación del propietario
- Cuenta de desarrollador de Charles Schwab: Aplicación de desarrollador aprobada con APIs de Trader y Market Data
Configuración local
-
Instala las dependencias:
npm install -
Configura las variables de desarrollo local:
cp .dev.vars.example .dev.varsCompleta
.dev.vars(ignorado por git):GITHUB_CLIENT_SECRET="your_github_client_secret" SCHWAB_CLIENT_ID="your_schwab_app_key" SCHWAB_CLIENT_SECRET="your_schwab_app_secret" -
Actualiza
wrangler.jsonc:- Establece
GITHUB_CLIENT_IDyALLOWED_GITHUB_USER_ID(encuéntralos mediantecurl -s https://api.github.com/users/<username> | grep '"id":'). - Establece
SCHWAB_REDIRECT_URIahttps://<worker-name>.<subdomain>.workers.dev/auth/schwab/callback. - Mantén
ENABLE_TRADING="false"durante el desarrollo.
- Establece
-
Ejecuta la validación automatizada:
npm run validate(Ejecuta typecheck, lint y la suite de Vitest en entornos DO de Workers simulados).
Despliegue en producción
Paso 1: Crear KV y establecer secretos
# 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_SECRET
Paso 2: Desplegar el Worker
npm run deploy
[!NOTE] La autorización MCP usa
AUTHORIZATION_VERSION = "client-permissions-v1". Actualizar esta versión invalida las concesiones activas, lo que requiere que los clientes se vuelvan a autenticar y elijan permisos. Los enlaces de cuenta de Schwab y las operaciones diferidas pendientes permanecen intactos. La actualización de S256 PKCE mantiene esta versión de autorización sin cambios: los tokens de acceso y actualización válidos existentes conservan sus permisos, incluidos los permisos emitidos sin PKCE. Los códigos de autorización emitidos antes de esta actualización deben reiniciar la autorización; los flujos de consentimiento pendientes o de devolución de llamada de GitHub sin parámetros S256 válidos también se rechazan. Reautorizar el mismo cliente aún revoca sus permisos anteriores antes de emitir un reemplazo.
Paso 3: Canal de CI/CD (GitHub Actions)
El repositorio incluye .github/workflows/deploy.yml para compilaciones automatizadas en main:
- Secretos del repositorio:
CLOUDFLARE_ACCOUNT_ID,CLOUDFLARE_API_TOKEN - Variables del repositorio:
PROD_OAUTH_KV_ID,PROD_ALLOWED_GITHUB_USER_ID,PROD_GITHUB_CLIENT_ID,PROD_SCHWAB_REDIRECT_URI,PROD_ENABLE_TRADING(falsepor defecto)
Ejecute npm run configure:deploy en CI para generar el wrangler.jsonc de implementación. ¡Nunca coloque secretos de cliente de Schwab o GitHub en GitHub Actions!
Paso 4: Autorizar Charles Schwab
Visite en su navegador:
https://<your-worker-domain>/auth/schwab/connect
- Autentíquese con GitHub (debe coincidir con
ALLOWED_GITHUB_USER_ID). - Inicie sesión en Schwab y apruebe el acceso a la cuenta.
- Los tokens se cifran de forma segura en el objeto duradero
SchwabVault.
Configuración del cliente
Punto final MCP: https://<your-worker-domain>/mcp
Cada nueva autorización OAuth de MCP requiere RFC 7636 S256 PKCE. Los metadatos del servidor anuncian solo S256; los clientes que carecen de soporte S256 deben actualizarse. Envíe exactamente un code_challenge (43 caracteres base64url sin relleno) y un code_challenge_method=S256 en la solicitud de autorización. Mantenga el verificador aleatorio generado independientemente en el cliente y envíelo como code_verifier solo al intercambiar el código de autorización. Los parámetros de desafío/método faltantes, duplicados o inválidos devuelven HTTP 400 invalid_request antes de iniciar el consentimiento. Los verificadores faltantes o incorrectos fallan en el intercambio de tokens a través del proveedor OAuth.
Las pruebas de protocolo local cubren el registro dinámico, el consentimiento del navegador, la verificación simulada del propietario de GitHub, el intercambio de códigos, la actualización y las llamadas MCP para ambos niveles de permiso. Estas pruebas no establecen compatibilidad con aplicaciones cliente reales; reconecte y verifique cada cliente después de la implementación.
Claude Desktop
Agregue a claude_desktop_config.json:
{
"mcpServers": {
"schwab": {
"url": "https://<your-worker-domain>/mcp"
}
}
}
Cursor / VS Code / Codex
Configure como servidor MCP remoto:
- Tipo:
Streamable HTTP - URL:
https://<your-worker-domain>/mcp - En la primera conexión, complete el flujo OAuth del navegador, seleccione Solo lectura o Acceso completo y verifique a través de GitHub.
Códigos de error y operaciones
| Código | Causa | Acción |
|---|---|---|
AUTH_REQUIRED | Cuenta de Schwab no vinculada | Visite reconnectUrl desde la respuesta |
REAUTHORIZATION_REQUIRED | Token de actualización caducado (7 días de inactividad) | Visite reconnectUrl para reautenticarse |
INVALID_ARGUMENT | El payload falló la validación de Zod | Revise la entrada contra src/schemas.ts |
RATE_LIMITED | Límite de velocidad de la API de Schwab alcanzado (HTTP 429) | Retroceda siguiendo retryAfterMs |
SCHWAB_REJECTED | Rechazo comercial ascendente | Verifique el ticker, el estado del pedido o el horario del mercado |
UPSTREAM_UNAVAILABLE | Error de puerta de enlace de Schwab o de objeto duradero | Reintente la solicitud (las solicitudes de lectura se reintentan una vez automáticamente) |
CONFIRMATION_INVALID | Desajuste del token de confirmación o frase incorrecta | Vuelva a ejecutar la confirmación con el texto exacto |
CONFIRMATION_EXPIRED | Token superó la validez de 5 minutos | Llame a prepare_trade nuevamente |
TRADE_STATE_CONFLICT | Propuesta ya consumida o tarea cancelada | Actualice el estado con list_delayed_trades |
DIRECT_TRADING_SESSION_INVALID | Sesión caducada (30 m inactiva / 8 h máxima) | Llame a enable_session_direct_trading |
EXECUTION_UNCERTAIN | Fallo de red durante el envío del pedido | No reintente a ciegas. Verifique a través de get_orders |
Monitoreo de registros en tiempo real
Transmita registros JSON estructurados y saneados a través de Cloudflare Workers Observability:
npx wrangler tail schwab-mcp --format json
Todos los encabezados, credenciales, tokens OAuth y números de cuenta se redactan automáticamente antes del registro.
Licencia
Licenciado bajo la Licencia Apache, Versión 2.0.