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 | 简体中文

TypeScript Cloudflare Workers Self-Hosted Glama Score MCP Vitest License

[!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.md y 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) o full_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 predeterminado false.
    • 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).

¿Por qué Cloudflare Workers? (Comparación)

CaracterísticaMCP local típico (Python/stdio)schwab-mcp (Este proyecto)
Alojamiento y costeEl 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 datosArchivos en texto plano en el disco localPrivado y autoalojado: tú posees tus credenciales en DO SQLite, cero SaaS de terceros
Ejecución y despliegueProceso en máquina local (muere al dormir/apagar)Cloudflare Workers Edge (serverless global 24/7, cero mantenimiento)
Acceso multi-clienteSolo escritorio únicoStreamable HTTP remoto (/mcp) en Cursor, Claude, VS Code y móvil
Almacenamiento de credencialesArchivos en texto plano en el disco localSQLite aislado dentro de Durable Object (SchwabVault), cero fugas a KV/registros
Permisos de clienteTodas las herramientas expuestas incondicionalmenteÁmbito OAuth dinámico (read_only vs full_access) por cliente
Seguridad de tradingNinguna o sugerencias débiles basadas en promptsInterruptor de apagado de despliegue (ENABLE_TRADING), repetición de frase en dos fases, capacidad de sesión
Órdenes fuera de horario / condicionalesImposible si el ordenador duermeDurable Object Alarms activan órdenes condicionales deterministas en la nube
Seguridad ante fallos de redRiesgo de doble ejecución en reintento ciegoEl 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 a get_account y get_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 a prepare_trade con acción PLACE para 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 a confirm_trade consumiendo 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:

HerramientaDescripciónEntradas clave
schwab_connection_statusVerificar conexión OAuth de Schwab y expiraciónNinguna
get_trading_diagnosticsVerificar registro de herramientas, interruptor de apagado y estado de trading activoNinguna (siempre visible)
get_quotesObtener cotizaciones de mercado en tiempo realsymbols (matriz de 1–50), fields, indicative
get_price_historyVelas OHLCV históricassymbol, periodType, frequencyType, frequency, startDate, endDate
get_option_chainCadenas de opciones, griegas y cotizacionessymbol, contractType (CALL/PUT/ALL), strikeCount, strategy
get_option_expiration_chainFechas de expiración para el subyacente de opcionessymbol
get_market_hoursHorarios de negociación del mercadomarkets (equity, option, bond, etc.), date (YYYY-MM-DD)
get_moversGanadores/perdedores/activos del mercadoindex ($DJI, $COMPX, $SPX), sort, frequency
search_instrumentsBuscar tickers o definiciones fundamentalessymbol, projection
list_accountsListar identificadores hash de cuenta (números reales eliminados)Ninguna
get_accountSaldos y posiciones para un hash de cuentaaccountHash, includePositions
get_ordersHistorial de ejecuciones en todas las cuentasfromEnteredTime, toEnteredTime (ISO-8601), status
get_orderDetalles de ejecución para una orden específicaaccountHash, orderId
get_transactionsFlujo de caja de cuenta y transacciones de tradingaccountHash, startDate, endDate (YYYY-MM-DD), types, symbol
get_user_preferencesContexto de perfil de usuario y permisos de tradingNinguna

Herramientas de control de trading (9 herramientas)

[!WARNING] Solo se registran para clientes a los que se les otorgó full_access durante la autorización OAuth. Ejecutar operaciones además requiere ENABLE_TRADING=true. La cancelación sigue disponible incluso cuando el trading está deshabilitado para reducir el riesgo de posiciones.

HerramientaDescripciónEntradas clave
enable_tradingVerificación de solo lectura del estado del interruptor de apagado de despliegueNinguna (el interruptor no se puede alternar vía MCP)
prepare_tradeCongelar instantánea de orden y emitir token de un solo uso de 5 minutosaccountHash, action (PLACE/REPLACE), executionMode, orderPayload, userInstruction
confirm_tradeConsumir token de confirmación con reproducción textualproposalId, confirmationToken, confirmationText
enable_session_direct_tradingHabilitar colocación directa de órdenes para la sesión actualuserInstruction (devuelve sessionCapability de revelación única)
place_order_directEnvío directo de órdenes sin confirmación por ordensessionCapability, executionKey (UUID), accountHash, orderPayload, userInstruction
disable_session_direct_tradingInvalidar capacidad de trading directo activa inmediatamentesessionCapability
cancel_orderCancelar orden de mercado en curso (sin confirmación necesaria)accountHash, orderId, userInstruction
list_delayed_tradesListar órdenes condicionales activas e históricasstatus (ACTIVE, EXECUTING, EXECUTED, etc.)
cancel_delayed_tradeCancelar tarea condicional activa inmediatamentetaskId, userInstruction

Salvaguardas de seguridad y protección

  1. 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.
  2. Confirmación en dos fases contra alucinaciones:
    • prepare_trade genera 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_trade consuma el token y ejecute la orden.
  3. 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 executionKey proporcionado por el cliente.
  4. Órdenes diferidas deterministas:
    • Reglas basadas en AST que combinan umbrales de TIME y QUOTE. Supervisadas por Durable Object Alarms.
  5. 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 con get_orders.
  6. 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

  1. Instala las dependencias:

    npm install
    
  2. Configura las variables de desarrollo local:

    cp .dev.vars.example .dev.vars
    

    Completa .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"
    
  3. Actualiza wrangler.jsonc:

    • Establece GITHUB_CLIENT_ID y ALLOWED_GITHUB_USER_ID (encuéntralos mediante curl -s https://api.github.com/users/<username> | grep '"id":').
    • Establece SCHWAB_REDIRECT_URI a https://<worker-name>.<subdomain>.workers.dev/auth/schwab/callback.
    • Mantén ENABLE_TRADING="false" durante el desarrollo.
  4. 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 (false por 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
  1. Autentíquese con GitHub (debe coincidir con ALLOWED_GITHUB_USER_ID).
  2. Inicie sesión en Schwab y apruebe el acceso a la cuenta.
  3. 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ódigoCausaAcción
AUTH_REQUIREDCuenta de Schwab no vinculadaVisite reconnectUrl desde la respuesta
REAUTHORIZATION_REQUIREDToken de actualización caducado (7 días de inactividad)Visite reconnectUrl para reautenticarse
INVALID_ARGUMENTEl payload falló la validación de ZodRevise la entrada contra src/schemas.ts
RATE_LIMITEDLímite de velocidad de la API de Schwab alcanzado (HTTP 429)Retroceda siguiendo retryAfterMs
SCHWAB_REJECTEDRechazo comercial ascendenteVerifique el ticker, el estado del pedido o el horario del mercado
UPSTREAM_UNAVAILABLEError de puerta de enlace de Schwab o de objeto duraderoReintente la solicitud (las solicitudes de lectura se reintentan una vez automáticamente)
CONFIRMATION_INVALIDDesajuste del token de confirmación o frase incorrectaVuelva a ejecutar la confirmación con el texto exacto
CONFIRMATION_EXPIREDToken superó la validez de 5 minutosLlame a prepare_trade nuevamente
TRADE_STATE_CONFLICTPropuesta ya consumida o tarea canceladaActualice el estado con list_delayed_trades
DIRECT_TRADING_SESSION_INVALIDSesión caducada (30 m inactiva / 8 h máxima)Llame a enable_session_direct_trading
EXECUTION_UNCERTAINFallo de red durante el envío del pedidoNo 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.