Initial commit: Hybrid MCP Light setup for Schwab
Some checks failed
Build and Push Docker Image / build (push) Failing after 48s

This commit is contained in:
2026-04-24 01:31:55 +00:00
commit 299b1bbc5e
11 changed files with 2315 additions and 0 deletions

16
.env.example Normal file
View File

@@ -0,0 +1,16 @@
# Schwab API Credentials (required)
# Get these from https://developer.schwab.com after app approval
SCHWAB_API_KEY=your_app_key
SCHWAB_APP_SECRET=your_app_secret
SCHWAB_CALLBACK_URL=https://127.0.0.1:8182
SCHWAB_TOKEN_PATH=/data/token.json
# Trading Permissions (optional)
# ALLOW_TRADING must be "true" to enable any write operations
# TRADING_ACCOUNTS: comma-separated account hashes, or "ALL"
# If ALLOW_TRADING=true but TRADING_ACCOUNTS is empty, no accounts can trade (safe default)
ALLOW_TRADING=false
TRADING_ACCOUNTS=
# Server Configuration (optional)
# PORT=8000

View File

@@ -0,0 +1,32 @@
name: Build and Push Docker Image
on:
push:
branches:
- main
- master
jobs:
build:
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Login to Gitea Container Registry
uses: docker/login-action@v2
with:
registry: gitea.ext.ben.io
username: ${{ gitea.actor }}
password: ${{ secrets.CR_PAT }}
- name: Build and Push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: gitea.ext.ben.io/${{ gitea.repository }}:latest
secrets: |
"ssh_key=${{ secrets.SSH_KEY }}"

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
__pycache__/
*.py[cod]
*$py.class
.venv/
.env
config.json
cookies.json
.pytest_cache/
.ruff_cache/

1
BLUEPRINT.md Symbolic link
View File

@@ -0,0 +1 @@
../BLUEPRINT.md

46
Dockerfile Normal file
View File

@@ -0,0 +1,46 @@
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder
ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy
WORKDIR /app
# Enable SSH for git dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
ssh-client \
&& rm -rf /var/lib/apt/lists/*
# Add gitea.ext.ben.io to known hosts
RUN mkdir -p -m 0700 ~/.ssh && ssh-keyscan git.local.ben.io >> ~/.ssh/known_hosts
# Install dependencies
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=secret,id=ssh_key,target=/root/.ssh/id_rsa \
uv sync --frozen --no-install-project --no-dev
# Copy the rest of the application
COPY . /app
# Install the project
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=secret,id=ssh_key,target=/root/.ssh/id_rsa \
uv sync --frozen --no-dev
FROM python:3.12-slim-bookworm
WORKDIR /app
# Copy the environment from the builder
COPY --from=builder /app /app
# Set up environment variables
ENV PATH="/app/.venv/bin:$PATH"
ENV PORT=8000
# Expose the port
EXPOSE 8000
# Run the server
CMD ["python", "server.py"]

1
README.md Normal file
View File

@@ -0,0 +1 @@
Schwab MCP exploration

30
compose.yaml Normal file
View File

@@ -0,0 +1,30 @@
include:
- ../deploy/base.yaml
services:
schwab-mcp:
<<: *mcp-service
<<: *mcp-healthcheck
image: gitea.ext.ben.io/b3nw/schwab-mcp-custom:latest
container_name: schwab-mcp
environment:
- SCHWAB_PLAYWRIGHT_URL=ws://schwab-browser:3000/playwright/chromium
- PORT=8000
volumes:
- ./cookies.json:/app/cookies.json
- ./config.json:/app/config.json
ports:
- "${PORT:-8160}:8000"
depends_on:
schwab-browser:
condition: service_started
schwab-browser:
image: ghcr.io/browserless/chromium:latest
container_name: schwab-browser
restart: unless-stopped
environment:
- TIMEOUT=300000
- MAX_CONCURRENT_SESSIONS=2
- PREBOOT_CHROME=true
shm_size: "1gb"

23
pyproject.toml Normal file
View File

@@ -0,0 +1,23 @@
[project]
name = "schwab-mcp-custom"
version = "0.1.0"
description = "Hybrid MCP Light server for Schwab scraper"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"mcp>=1.2.0",
"fastmcp>=0.4.1",
"starlette>=0.41.0",
"uvicorn>=0.32.0",
"schwab-scraper @ git+ssh://gitea@git.local.ben.io/b3nw/schwab-scraper.git",
]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = []

View File

131
server.py Normal file
View File

@@ -0,0 +1,131 @@
import json
import os
from typing import Optional, Any
from fastmcp import FastMCP
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route, Mount
import uvicorn
# Import the unified API from the schwab_scraper dependency
import schwab_scraper.unified_api as api
# Initialize FastMCP
mcp = FastMCP("SchwabScraper")
@mcp.tool()
async def get_session_status(debug: bool = False) -> str:
"""Get the current session status of the Schwab scraper.
Args:
debug: Enable debug logging
"""
result = await api.get_session_status(debug=debug)
return json.dumps(result)
@mcp.tool()
async def list_accounts(debug: bool = False) -> str:
"""List all Schwab accounts.
Args:
debug: Enable debug logging
"""
result = await api.list_accounts(debug=debug)
return json.dumps(result)
@mcp.tool()
async def get_account_overview(account: Optional[str] = None, debug: bool = False) -> str:
"""Get the overview for a specific account.
Args:
account: Account summary or ID (optional)
debug: Enable debug logging
"""
result = await api.get_account_overview(account=account, debug=debug)
return json.dumps(result)
@mcp.tool()
async def get_positions(account: Optional[str] = None, include_non_equity: bool = False, debug: bool = False) -> str:
"""Get positions for a specific account.
Args:
account: Account summary or ID (optional)
include_non_equity: Whether to include non-equity positions
debug: Enable debug logging
"""
result = await api.get_positions(account=account, include_non_equity=include_non_equity, debug=debug)
return json.dumps(result)
@mcp.tool()
async def get_transactions(
account: Optional[str] = None,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
time_period: Optional[str] = None,
debug: bool = False
) -> str:
"""Get transaction history.
Args:
account: Account ID (optional)
start_date: Start date for transactions (optional)
end_date: End date for transactions (optional)
time_period: Time period (e.g., '1D', '1M') (optional)
debug: Enable debug logging
"""
result = await api.get_transaction_history(
account=account,
start_date=start_date,
end_date=end_date,
time_period=time_period,
debug=debug
)
return json.dumps(result)
@mcp.tool()
async def get_morningstar_data(ticker: str, debug: bool = False) -> str:
"""Get Morningstar data for a ticker.
Args:
ticker: Stock ticker symbol
debug: Enable debug logging
"""
result = await api.get_morningstar_data(ticker, debug=debug)
return json.dumps(result)
@mcp.tool()
async def api_call(endpoint: str, method: str = "GET", params: str = "{}") -> str:
"""Executes a raw API call to the Schwab service (Dummy implementation).
Refer to the 'api-reference' resource for available endpoints and parameters.
Args:
endpoint: The API path
method: HTTP method (GET, POST, etc.)
params: JSON string of parameters/body
"""
return json.dumps({"status": "not_implemented", "message": "API pass-through not supported for scraper"})
@mcp.resource("service://api-reference")
def get_api_docs() -> str:
"""Returns the API documentation for using the 'api_call' tool."""
return "Schwab Scraper MCP Server - Unified API Documentation\n\nThis server provides tools to interact with Schwab accounts via scraping. The 'api_call' tool is a placeholder."
async def health(request):
"""Health check endpoint."""
return JSONResponse({"status": "ok"})
# Create the Starlette application
mcp_app = mcp.http_app()
app = Starlette(
routes=[
Route("/health", health),
Mount("/", app=mcp_app)
],
lifespan=mcp_app.lifespan
)
if __name__ == "__main__":
port = int(os.getenv("PORT", 8160))
uvicorn.run(app, host="0.0.0.0", port=port)

2026
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff