schwab_mcp 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.claude/settings.json +14 -0
- data/CLAUDE.md +124 -0
- data/debug_env.rb +46 -0
- data/doc/DATA_OBJECTS_MIGRATION_TODO.md +80 -0
- data/doc/SCHWAB_CLIENT_FACTORY_REFACTOR_PLAN.md +187 -0
- data/exe/schwab_mcp +14 -3
- data/exe/schwab_token_refresh +10 -9
- data/lib/schwab_mcp/redactor.rb +4 -0
- data/lib/schwab_mcp/schwab_client_factory.rb +44 -0
- data/lib/schwab_mcp/tools/cancel_order_tool.rb +29 -50
- data/lib/schwab_mcp/tools/get_market_hours_tool.rb +27 -28
- data/lib/schwab_mcp/tools/get_order_tool.rb +51 -108
- data/lib/schwab_mcp/tools/get_price_history_tool.rb +23 -35
- data/lib/schwab_mcp/tools/help_tool.rb +1 -22
- data/lib/schwab_mcp/tools/list_account_orders_tool.rb +35 -63
- data/lib/schwab_mcp/tools/list_account_transactions_tool.rb +43 -72
- data/lib/schwab_mcp/tools/list_movers_tool.rb +21 -34
- data/lib/schwab_mcp/tools/list_schwab_accounts_tool.rb +18 -31
- data/lib/schwab_mcp/tools/option_chain_tool.rb +130 -82
- data/lib/schwab_mcp/tools/place_order_tool.rb +105 -117
- data/lib/schwab_mcp/tools/preview_order_tool.rb +100 -48
- data/lib/schwab_mcp/tools/quote_tool.rb +33 -26
- data/lib/schwab_mcp/tools/quotes_tool.rb +97 -45
- data/lib/schwab_mcp/tools/replace_order_tool.rb +104 -116
- data/lib/schwab_mcp/tools/schwab_account_details_tool.rb +56 -72
- data/lib/schwab_mcp/version.rb +1 -1
- data/lib/schwab_mcp.rb +1 -2
- data/orders_example.json +7084 -0
- data/spx_option_chain.json +25073 -0
- data/test_mcp.rb +16 -0
- data/test_server.rb +23 -0
- data/trading_brokerage_account_details.json +89 -0
- data/transactions_example.json +488 -0
- metadata +17 -7
- data/lib/schwab_mcp/option_chain_filter.rb +0 -213
- data/lib/schwab_mcp/orders/iron_condor_order.rb +0 -87
- data/lib/schwab_mcp/orders/order_factory.rb +0 -40
- data/lib/schwab_mcp/orders/vertical_order.rb +0 -62
- data/lib/schwab_mcp/tools/option_strategy_finder_tool.rb +0 -378
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7680eeeed2f4a22cf70f836af9ab019c60e6de76867408acf79d0ffdd3217b33
|
4
|
+
data.tar.gz: e236d0f991b031b84873fbc8244b623dd74603ac3ab043d3586a987a8656eb37
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 678fb6af3f9de96315e9798fa5f8aebd4dde3bcc102859b7c3e761aefb2b7d3ec591b56267978936114099a68f746a3c43b0a72abf4bd711c83822758053f63f
|
7
|
+
data.tar.gz: ae505ca27dbf4f8579279a05845befed68fc6c1690cdd9d469b585239827e931e3726381795d60fb8c086d2de70fdfc0966fc24ea4b9f380c3f4311846a4ec58
|
@@ -0,0 +1,14 @@
|
|
1
|
+
{
|
2
|
+
"permissions": {
|
3
|
+
"allow": [
|
4
|
+
"Bash(bundle exec rspec:*)",
|
5
|
+
"Bash(bundle exec rubocop:*)",
|
6
|
+
"Bash(git checkout:*)"
|
7
|
+
],
|
8
|
+
"deny": [],
|
9
|
+
"additionalDirectories": [
|
10
|
+
"/Users/jplatta/repos/schwab_rb",
|
11
|
+
"/Users/jplatta/repos/options_trader"
|
12
|
+
]
|
13
|
+
}
|
14
|
+
}
|
data/CLAUDE.md
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
# CLAUDE.md
|
2
|
+
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
4
|
+
|
5
|
+
## Common Development Commands
|
6
|
+
|
7
|
+
### Testing
|
8
|
+
- Run all tests: `bundle exec rspec`
|
9
|
+
- Run specific test: `bundle exec rspec spec/tools/specific_tool_spec.rb`
|
10
|
+
- NEVER use `-v` flag with RSpec - it clutters output unnecessarily
|
11
|
+
- Reference @.github/instructions/ruby-testing.instructions.md when writing unit tests
|
12
|
+
|
13
|
+
### Code Quality
|
14
|
+
- Run RuboCop linter: `bundle exec rubocop`
|
15
|
+
- Default rake task (tests + linting): `rake`
|
16
|
+
|
17
|
+
### Server Operations
|
18
|
+
- Start MCP server: `bundle exec exe/schwab_mcp`
|
19
|
+
- Alternative server start: `./start_mcp_server.sh`
|
20
|
+
- Token refresh: `bundle exec exe/schwab_token_refresh`
|
21
|
+
- Token reset: `bundle exec exe/schwab_token_reset`
|
22
|
+
|
23
|
+
### Development Setup
|
24
|
+
- Initial setup: `bin/setup`
|
25
|
+
- Install dependencies: `bundle install`
|
26
|
+
- Interactive console: `bin/console`
|
27
|
+
|
28
|
+
## Architecture Overview
|
29
|
+
|
30
|
+
### Core Structure
|
31
|
+
This is a Ruby gem providing a Model Context Protocol (MCP) server for Schwab brokerage API integration. The architecture follows these key patterns:
|
32
|
+
|
33
|
+
- **MCP Tools Pattern**: All functionality is exposed through MCP tools extending `MCP::Tool`
|
34
|
+
- **Data Objects Migration**: Currently migrating from JSON parsing to schwab_rb data objects (see `doc/DATA_OBJECTS_MIGRATION_TODO.md`)
|
35
|
+
- **Shared Logging**: Uses singleton logger pattern with `Loggable` module across all components
|
36
|
+
|
37
|
+
### Key Components
|
38
|
+
- `lib/schwab_mcp.rb` - Main server class and tool registration
|
39
|
+
- `lib/schwab_mcp/tools/` - All MCP tools (17 tools total)
|
40
|
+
- `lib/schwab_mcp/orders/` - Order construction utilities (Iron Condor, Vertical spreads)
|
41
|
+
- `lib/schwab_mcp/loggable.rb` - Shared logging functionality
|
42
|
+
- `lib/schwab_mcp/redactor.rb` - Sensitive data redaction for logs
|
43
|
+
|
44
|
+
### Tool Categories
|
45
|
+
- **Account Tools**: `schwab_account_details_tool`, `list_schwab_accounts_tool`
|
46
|
+
- **Order Tools**: `list_account_orders_tool`, `get_order_tool`, `cancel_order_tool`, `preview_order_tool`, `place_order_tool`, `replace_order_tool`
|
47
|
+
- **Market Data Tools**: `quote_tool`, `quotes_tool`, `option_chain_tool`, `list_movers_tool`, `get_market_hours_tool`, `get_price_history_tool`
|
48
|
+
- **Transaction Tools**: `list_account_transactions_tool`
|
49
|
+
- **Utility Tools**: `help_tool`
|
50
|
+
|
51
|
+
## Data Objects Migration
|
52
|
+
|
53
|
+
**CRITICAL**: This codebase is currently migrating from manual JSON parsing to schwab_rb data objects. Progress is tracked in `doc/DATA_OBJECTS_MIGRATION_TODO.md` (6/17 tools completed as of July 2025).
|
54
|
+
|
55
|
+
### Migration Pattern
|
56
|
+
When working on tools, follow this pattern:
|
57
|
+
1. Replace `JSON.parse(response.body)` with direct data object usage
|
58
|
+
2. Change hash access `data['key']` to object methods `object.key`
|
59
|
+
3. Remove `JSON::ParserError` rescue blocks
|
60
|
+
4. Update formatting to use data object attributes
|
61
|
+
5. Write comprehensive RSpec tests with proper mocking
|
62
|
+
|
63
|
+
### Completed Migrations
|
64
|
+
Tools using data objects (safe to follow as examples):
|
65
|
+
- `schwab_account_details_tool.rb` - Uses `Account` and `AccountNumbers`
|
66
|
+
- `list_schwab_accounts_tool.rb` - Uses `AccountNumbers`
|
67
|
+
- `list_account_orders_tool.rb` - Uses `Order` and `AccountNumbers`
|
68
|
+
- `get_order_tool.rb` - Uses `Order`
|
69
|
+
- `cancel_order_tool.rb` - Uses `Order`
|
70
|
+
- `preview_order_tool.rb` - Uses `OrderPreview`
|
71
|
+
|
72
|
+
## Development Conventions
|
73
|
+
|
74
|
+
### Ruby Style
|
75
|
+
- Use `frozen_string_literal: true` in all files
|
76
|
+
- Follow snake_case/PascalCase conventions
|
77
|
+
- Use `require_relative` for local dependencies
|
78
|
+
- Prefer keyword arguments for multi-parameter methods
|
79
|
+
- Always use descriptive variable names
|
80
|
+
|
81
|
+
### Tool Development
|
82
|
+
- All tools extend `MCP::Tool` and include `Loggable`
|
83
|
+
- Use proper JSON Schema validation in `input_schema`
|
84
|
+
- Return `MCP::Tool::Response` objects with structured content
|
85
|
+
- Include descriptive annotations (title, read_only_hint, etc.)
|
86
|
+
|
87
|
+
### Testing Strategy
|
88
|
+
- Mock environment variables globally in `spec/spec_helper.rb`
|
89
|
+
- Test both success and error scenarios
|
90
|
+
- Mock schwab_rb client and data objects appropriately
|
91
|
+
- File naming: `*_spec.rb` for corresponding `*.rb`
|
92
|
+
|
93
|
+
### Environment Variables
|
94
|
+
Required for operation:
|
95
|
+
- `SCHWAB_API_KEY`, `SCHWAB_APP_SECRET`, `SCHWAB_CALLBACK_URI`, `TOKEN_PATH`
|
96
|
+
- Account mappings: `*_ACCOUNT` (e.g., `TRADING_BROKERAGE_ACCOUNT`)
|
97
|
+
- Optional: `LOG_LEVEL`, `LOGFILE`
|
98
|
+
|
99
|
+
### Git Workflow
|
100
|
+
- Stage specific files, never use `git add .`
|
101
|
+
- Write descriptive commits with progress tracking
|
102
|
+
- Include 1-3 bullet points for detailed changes
|
103
|
+
- Commit only relevant files, exclude temporary/log files
|
104
|
+
|
105
|
+
## Dependencies
|
106
|
+
|
107
|
+
### Core Dependencies
|
108
|
+
- `schwab_rb` - Schwab API client with data objects (prefer `return_data_objects: true`)
|
109
|
+
- `mcp` - Model Context Protocol framework
|
110
|
+
- `rspec` - Testing framework
|
111
|
+
- `rubocop` - Code style enforcement
|
112
|
+
|
113
|
+
### External Integrations
|
114
|
+
- Schwab Trading API for all brokerage operations
|
115
|
+
- OAuth token management for authentication
|
116
|
+
- MCP protocol for AI assistant integration
|
117
|
+
|
118
|
+
## Important Notes
|
119
|
+
|
120
|
+
- **Security**: Never commit real credentials or account numbers
|
121
|
+
- **Logging**: Use `Redactor` class for sensitive data in debug output
|
122
|
+
- **Data Objects**: Always prefer schwab_rb data objects over JSON parsing
|
123
|
+
- **Testing**: All tests must pass before committing changes
|
124
|
+
- **Tool Design**: Tools should be read-only unless explicitly destructive
|
data/debug_env.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# Test script to debug environment variables and client initialization
|
4
|
+
|
5
|
+
puts "=== Environment Variables ==="
|
6
|
+
puts "SCHWAB_API_KEY: #{ENV['SCHWAB_API_KEY'] ? '[SET]' : '[NOT SET]'}"
|
7
|
+
puts "SCHWAB_APP_SECRET: #{ENV['SCHWAB_APP_SECRET'] ? '[SET]' : '[NOT SET]'}"
|
8
|
+
puts "APP_CALLBACK_URL: #{ENV['APP_CALLBACK_URL'] ? '[SET]' : '[NOT SET]'}"
|
9
|
+
puts "TOKEN_PATH: #{ENV['TOKEN_PATH'] ? '[SET]' : '[NOT SET]'}"
|
10
|
+
|
11
|
+
puts "\n=== Testing Token File ==="
|
12
|
+
token_path = ENV['TOKEN_PATH'] || './token.json'
|
13
|
+
puts "Checking token file at: #{token_path}"
|
14
|
+
puts "Token file exists: #{File.exist?(token_path)}"
|
15
|
+
|
16
|
+
if File.exist?(token_path)
|
17
|
+
begin
|
18
|
+
content = File.read(token_path)
|
19
|
+
require 'json'
|
20
|
+
data = JSON.parse(content)
|
21
|
+
puts "Token file valid JSON: true"
|
22
|
+
puts "Token has access_token: #{data.dig('token', 'access_token') ? '[YES]' : '[NO]'}"
|
23
|
+
puts "Token has refresh_token: #{data.dig('token', 'refresh_token') ? '[YES]' : '[NO]'}"
|
24
|
+
rescue => e
|
25
|
+
puts "Token file JSON error: #{e.message}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
puts "\n=== Testing Client Initialization ==="
|
30
|
+
begin
|
31
|
+
require 'schwab_rb'
|
32
|
+
require_relative 'lib/schwab_mcp/schwab_client_factory'
|
33
|
+
|
34
|
+
client = SchwabMCP::SchwabClientFactory.create_client
|
35
|
+
|
36
|
+
if client
|
37
|
+
puts "Client initialized: SUCCESS"
|
38
|
+
puts "Client class: #{client.class}"
|
39
|
+
else
|
40
|
+
puts "Client initialized: FAILED (nil returned)"
|
41
|
+
end
|
42
|
+
rescue => e
|
43
|
+
puts "Client initialization error: #{e.message}"
|
44
|
+
puts "Backtrace:"
|
45
|
+
puts e.backtrace.first(5).join("\n")
|
46
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# SchwabMCP Data Objects Migration TODO
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
Update all tools in `lib/schwab_mcp/tools/` to use data objects from schwab_rb instead of parsing JSON responses directly.
|
5
|
+
|
6
|
+
## Migration Date
|
7
|
+
July 21, 2025
|
8
|
+
|
9
|
+
**Progress: 17/17 tools completed**
|
10
|
+
|
11
|
+
## Tools to Update
|
12
|
+
|
13
|
+
### ✅ Account-related Tools
|
14
|
+
- [x] **schwab_account_details_tool.rb** - ✅ COMPLETED - Updated to use Account and AccountNumbers data objects
|
15
|
+
- [x] **list_schwab_accounts_tool.rb** - ✅ COMPLETED - Updated to use AccountNumbers data object
|
16
|
+
|
17
|
+
### ✅ Order-related Tools
|
18
|
+
- [x] **list_account_orders_tool.rb** - ✅ COMPLETED - Updated to use Order and AccountNumbers data objects
|
19
|
+
- [x] **get_order_tool.rb** - ✅ COMPLETED - Updated to use Order data object
|
20
|
+
- [x] **cancel_order_tool.rb** - ✅ COMPLETED - Updated to use Order data object
|
21
|
+
- [x] **preview_order_tool.rb** - ✅ COMPLETED - Updated to use OrderPreview data object
|
22
|
+
- [x] **place_order_tool.rb** - ✅ COMPLETED - Updated to use AccountNumbers data object for account resolution
|
23
|
+
- [x] **replace_order_tool.rb** - ✅ COMPLETED - Updated to use AccountNumbers data object for account resolution
|
24
|
+
|
25
|
+
### ✅ Market Data Tools
|
26
|
+
- [x] **quote_tool.rb** - ✅ COMPLETED - Updated to use EquityQuote, OptionQuote, and IndexQuote data objects
|
27
|
+
- [x] **quotes_tool.rb** - ✅ COMPLETED - Updated to use EquityQuote, OptionQuote, and IndexQuote data objects
|
28
|
+
- [x] **option_chain_tool.rb** - Update to use OptionChain data object
|
29
|
+
- [x] **list_movers_tool.rb** - ✅ COMPLETED - Updated to use MarketMovers data object
|
30
|
+
- [x] **get_market_hours_tool.rb** - Update to use MarketHours data object
|
31
|
+
- [x] **get_price_history_tool.rb** - Update to use PriceHistory data object
|
32
|
+
|
33
|
+
### ✅ Transaction Tools
|
34
|
+
- [x] **list_account_transactions_tool.rb** - Update to use Transaction data objects
|
35
|
+
|
36
|
+
### ✅ Strategy Tools
|
37
|
+
- [x] **option_strategy_finder_tool.rb** - ✅ COMPLETED - Updated to use OptionChain data object with refactored OptionChainFilter
|
38
|
+
|
39
|
+
### ✅ Utility Tools
|
40
|
+
- [x] **help_tool.rb** - No changes needed (utility tool)
|
41
|
+
|
42
|
+
## Total Tools Found: 17
|
43
|
+
|
44
|
+
## Migration Steps for Each Tool
|
45
|
+
|
46
|
+
1. **Remove JSON parsing logic** - Delete manual hash key access
|
47
|
+
2. **Use data object attributes** - Access data via object methods/attributes
|
48
|
+
3. **Update error handling** - Ensure proper handling of data object responses
|
49
|
+
4. **Write minimal unit tests** - Verify tool works with data objects
|
50
|
+
5. **Test functionality** - Manual testing if needed
|
51
|
+
6. **Clean commit** - Make focused commit for each tool
|
52
|
+
|
53
|
+
## Data Object Mappings
|
54
|
+
|
55
|
+
Based on the schwab_rb data objects available:
|
56
|
+
|
57
|
+
- **Account data** → `SchwabRb::DataObjects::Account`
|
58
|
+
- **Account numbers** → `SchwabRb::DataObjects::AccountNumbers`
|
59
|
+
- **Orders** → `SchwabRb::DataObjects::Order`
|
60
|
+
- **Order preview** → `SchwabRb::DataObjects::OrderPreview`
|
61
|
+
- **Quotes** → `SchwabRb::DataObjects::Quote`
|
62
|
+
- **Option chains** → `SchwabRb::DataObjects::OptionChain`
|
63
|
+
- **Transactions** → `SchwabRb::DataObjects::Transaction`
|
64
|
+
- **Positions** → `SchwabRb::DataObjects::Position`
|
65
|
+
- **Market hours** → `SchwabRb::DataObjects::MarketHours` (if available)
|
66
|
+
- **Price history** → `SchwabRb::DataObjects::PriceHistory` (if available)
|
67
|
+
|
68
|
+
## Notes
|
69
|
+
|
70
|
+
- All schwab_rb client methods now default to `return_data_objects: true`
|
71
|
+
- Maintain backward compatibility where possible
|
72
|
+
- Focus on cleaner, more maintainable code
|
73
|
+
- Each tool update should be a separate, focused commit
|
74
|
+
- Write minimal unit tests for each tool to prevent regressions
|
75
|
+
|
76
|
+
## Progress Tracking
|
77
|
+
|
78
|
+
- **Total tools to update**: 16 (excluding help_tool.rb)
|
79
|
+
- **Tools completed**: 17
|
80
|
+
- **Tools remaining**: 0
|
@@ -0,0 +1,187 @@
|
|
1
|
+
# Schwab Client Factory Refactor Plan
|
2
|
+
|
3
|
+
## Problem Statement
|
4
|
+
|
5
|
+
Every tool in the schwab_mcp codebase has identical client initialization code repeated 17 times across the project:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
client = SchwabRb::Auth.init_client_easy(
|
9
|
+
ENV['SCHWAB_API_KEY'],
|
10
|
+
ENV['SCHWAB_APP_SECRET'],
|
11
|
+
ENV['SCHWAB_CALLBACK_URI'],
|
12
|
+
ENV['TOKEN_PATH']
|
13
|
+
)
|
14
|
+
|
15
|
+
unless client
|
16
|
+
log_error("Failed to initialize Schwab client")
|
17
|
+
return MCP::Tool::Response.new([{
|
18
|
+
type: "text",
|
19
|
+
text: "**Error**: Failed to initialize Schwab client. Check your credentials."
|
20
|
+
}])
|
21
|
+
end
|
22
|
+
```
|
23
|
+
|
24
|
+
**Files affected (18 total):**
|
25
|
+
- All 17 tools in `lib/schwab_mcp/tools/`
|
26
|
+
- `exe/schwab_token_refresh`
|
27
|
+
- `debug_env.rb`
|
28
|
+
|
29
|
+
This creates ~170 lines of duplicated code and makes maintenance difficult.
|
30
|
+
|
31
|
+
## Proposed Solution: SchwabClientFactory Module
|
32
|
+
|
33
|
+
### Design Goals
|
34
|
+
1. **Centralize client creation** - Single location for all client initialization logic
|
35
|
+
2. **Handle error scenarios** - Consistent error handling and logging across tools
|
36
|
+
3. **Support caching** - Optional client caching to avoid repeated auth calls
|
37
|
+
4. **Integrate with existing patterns** - Uses existing `Loggable` module
|
38
|
+
5. **Maintain compatibility** - Drop-in replacement requiring minimal tool changes
|
39
|
+
|
40
|
+
### Implementation
|
41
|
+
|
42
|
+
#### Step 1: Create SchwabClientFactory Module
|
43
|
+
|
44
|
+
**File**: `lib/schwab_mcp/schwab_client_factory.rb`
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
# frozen_string_literal: true
|
48
|
+
|
49
|
+
require_relative "loggable"
|
50
|
+
|
51
|
+
module SchwabMCP
|
52
|
+
module SchwabClientFactory
|
53
|
+
extend Loggable
|
54
|
+
|
55
|
+
# Creates a new Schwab client with standard error handling
|
56
|
+
def self.create_client
|
57
|
+
begin
|
58
|
+
log_debug("Initializing Schwab client")
|
59
|
+
client = SchwabRb::Auth.init_client_easy(
|
60
|
+
ENV['SCHWAB_API_KEY'],
|
61
|
+
ENV['SCHWAB_APP_SECRET'],
|
62
|
+
ENV['SCHWAB_CALLBACK_URI'],
|
63
|
+
ENV['TOKEN_PATH']
|
64
|
+
)
|
65
|
+
|
66
|
+
unless client
|
67
|
+
log_error("Failed to initialize Schwab client - check credentials")
|
68
|
+
return nil
|
69
|
+
end
|
70
|
+
|
71
|
+
log_debug("Schwab client initialized successfully")
|
72
|
+
client
|
73
|
+
rescue => e
|
74
|
+
log_error("Error initializing Schwab client: #{e.message}")
|
75
|
+
log_debug("Backtrace: #{e.backtrace.first(3).join('\n')}")
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Optional: Cached client for performance (use with caution for token expiry)
|
81
|
+
def self.cached_client
|
82
|
+
@client ||= create_client
|
83
|
+
end
|
84
|
+
|
85
|
+
# Helper method for consistent error responses
|
86
|
+
def self.client_error_response
|
87
|
+
MCP::Tool::Response.new([{
|
88
|
+
type: "text",
|
89
|
+
text: "**Error**: Failed to initialize Schwab client. Check your credentials."
|
90
|
+
}])
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
#### Step 2: Update Main Module
|
97
|
+
|
98
|
+
Add to `lib/schwab_mcp.rb`:
|
99
|
+
```ruby
|
100
|
+
require_relative "schwab_mcp/schwab_client_factory"
|
101
|
+
```
|
102
|
+
|
103
|
+
#### Step 3: Refactor Tool Pattern
|
104
|
+
|
105
|
+
**Before (10+ lines per tool):**
|
106
|
+
```ruby
|
107
|
+
begin
|
108
|
+
client = SchwabRb::Auth.init_client_easy(
|
109
|
+
ENV['SCHWAB_API_KEY'],
|
110
|
+
ENV['SCHWAB_APP_SECRET'],
|
111
|
+
ENV['SCHWAB_CALLBACK_URI'],
|
112
|
+
ENV['TOKEN_PATH']
|
113
|
+
)
|
114
|
+
|
115
|
+
unless client
|
116
|
+
log_error("Failed to initialize Schwab client")
|
117
|
+
return MCP::Tool::Response.new([{
|
118
|
+
type: "text",
|
119
|
+
text: "**Error**: Failed to initialize Schwab client. Check your credentials."
|
120
|
+
}])
|
121
|
+
end
|
122
|
+
|
123
|
+
# tool logic here...
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
**After (3 lines per tool):**
|
128
|
+
```ruby
|
129
|
+
client = SchwabClientFactory.create_client
|
130
|
+
return SchwabClientFactory.client_error_response unless client
|
131
|
+
|
132
|
+
# tool logic here...
|
133
|
+
```
|
134
|
+
|
135
|
+
#### Step 4: Update Tool Imports
|
136
|
+
|
137
|
+
Add to each tool file:
|
138
|
+
```ruby
|
139
|
+
require_relative "../schwab_client_factory"
|
140
|
+
```
|
141
|
+
|
142
|
+
### Migration Strategy
|
143
|
+
|
144
|
+
1. **Create the factory module** first
|
145
|
+
2. **Update one tool** as a proof of concept
|
146
|
+
3. **Test thoroughly** to ensure no regression
|
147
|
+
4. **Batch update remaining tools** (can be done efficiently with MultiEdit)
|
148
|
+
5. **Update executable files** (`exe/schwab_token_refresh`, `debug_env.rb`)
|
149
|
+
6. **Remove old client initialization code**
|
150
|
+
|
151
|
+
### Benefits
|
152
|
+
|
153
|
+
1. **Code Reduction**: ~170 lines of duplicated code → 1 centralized module
|
154
|
+
2. **Consistent Error Handling**: All tools handle auth failures identically
|
155
|
+
3. **Easier Maintenance**: Auth logic changes in one place
|
156
|
+
4. **Better Testing**: Mock client creation in one place
|
157
|
+
5. **Performance Options**: Can add client caching if needed
|
158
|
+
6. **Future Extensibility**: Easy to add retry logic, connection pooling, etc.
|
159
|
+
|
160
|
+
### Risks and Considerations
|
161
|
+
|
162
|
+
1. **Token Caching**: Cached clients may hold expired tokens - use with caution
|
163
|
+
2. **Environment Variables**: Factory assumes same env vars for all tools (currently true)
|
164
|
+
3. **Error Handling**: Must ensure all tools properly check for nil client response
|
165
|
+
4. **Testing**: All tool tests will need to mock `SchwabClientFactory.create_client`
|
166
|
+
|
167
|
+
### Testing Strategy
|
168
|
+
|
169
|
+
1. **Unit tests** for SchwabClientFactory module
|
170
|
+
2. **Integration tests** to ensure tools still work correctly
|
171
|
+
3. **Mock the factory** in existing tool tests rather than individual client creation
|
172
|
+
4. **Verify error scenarios** are handled consistently
|
173
|
+
|
174
|
+
### Files to Modify
|
175
|
+
|
176
|
+
**New files:**
|
177
|
+
- `lib/schwab_mcp/schwab_client_factory.rb`
|
178
|
+
|
179
|
+
**Modified files:**
|
180
|
+
- `lib/schwab_mcp.rb` (add require)
|
181
|
+
- All 17 tool files in `lib/schwab_mcp/tools/`
|
182
|
+
- `exe/schwab_token_refresh`
|
183
|
+
- `debug_env.rb`
|
184
|
+
|
185
|
+
**Total impact:** 20 files modified, ~150 lines of code removed, 1 new module added
|
186
|
+
|
187
|
+
This refactor will significantly improve code maintainability while preserving all existing functionality.
|
data/exe/schwab_mcp
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
-
|
4
|
-
require "dotenv
|
3
|
+
require_relative "../lib/schwab_mcp"
|
4
|
+
require "dotenv"
|
5
|
+
|
6
|
+
Dotenv.load
|
5
7
|
|
6
8
|
required_vars = [
|
7
9
|
'SCHWAB_API_KEY',
|
@@ -12,8 +14,17 @@ required_vars = [
|
|
12
14
|
missing_vars = required_vars.select { |var| ENV[var].nil? || ENV[var].empty? }
|
13
15
|
|
14
16
|
unless missing_vars.empty?
|
17
|
+
STDERR.puts "[SCRIPT] Missing required environment variables: #{missing_vars.join(', ')}"
|
15
18
|
exit 1
|
16
19
|
end
|
17
20
|
|
21
|
+
STDERR.puts "[SCRIPT] All required environment variables are set."
|
18
22
|
|
19
|
-
|
23
|
+
begin
|
24
|
+
STDERR.puts "[SCRIPT] Starting Schwab MCP server..."
|
25
|
+
SchwabMCP::Server.new.start
|
26
|
+
rescue StandardError => e
|
27
|
+
STDERR.puts "[SCRIPT] Error starting Schwab MCP server: #{e.message}"
|
28
|
+
STDERR.puts e.backtrace.join("\n")
|
29
|
+
exit 1
|
30
|
+
end
|
data/exe/schwab_token_refresh
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
require 'pry'
|
6
6
|
require 'dotenv'
|
7
7
|
require 'schwab_rb'
|
8
|
+
require_relative '../lib/schwab_mcp/schwab_client_factory'
|
8
9
|
|
9
10
|
Dotenv.load
|
10
11
|
|
@@ -17,7 +18,7 @@ required_vars = [
|
|
17
18
|
missing_vars = required_vars.select { |var| ENV[var].nil? || ENV[var].empty? }
|
18
19
|
|
19
20
|
unless missing_vars.empty?
|
20
|
-
puts "
|
21
|
+
puts "Missing required environment variables: #{missing_vars.join(', ')}"
|
21
22
|
exit 1
|
22
23
|
end
|
23
24
|
|
@@ -25,14 +26,14 @@ token_path = ENV['TOKEN_PATH']
|
|
25
26
|
puts "Token path: #{token_path}"
|
26
27
|
|
27
28
|
begin
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
29
|
+
client = SchwabMCP::SchwabClientFactory.create_client
|
30
|
+
if client
|
31
|
+
puts "Token refresh completed successfully"
|
32
|
+
else
|
33
|
+
puts "Token refresh failed: Unable to create client"
|
34
|
+
exit 1
|
35
|
+
end
|
35
36
|
rescue => e
|
36
|
-
puts "
|
37
|
+
puts "Token refresh failed: #{e.message}"
|
37
38
|
exit 1
|
38
39
|
end
|
data/lib/schwab_mcp/redactor.rb
CHANGED
@@ -89,6 +89,10 @@ module SchwabMCP
|
|
89
89
|
redacted.gsub!(/account[_\s]*number[_\s]*[:\=]\s*\d{8,9}/i, "account_number: #{REDACTED_ACCOUNT_PLACEHOLDER}")
|
90
90
|
redacted.gsub!(/account[_\s]*id[_\s]*[:\=]\s*\d{8,9}/i, "account_id: #{REDACTED_ACCOUNT_PLACEHOLDER}")
|
91
91
|
|
92
|
+
# Redact long hashes (40+ hex chars) in URLs/logs (e.g., Schwab account hashes)
|
93
|
+
# Example: /accounts/4996EA061B4878E8D0B9063DF74925E5688F475BE00AF6A0A41E1FC4A2510CA0/
|
94
|
+
redacted.gsub!(/\b[0-9a-fA-F]{40,}\b/, REDACTED_HASH_PLACEHOLDER)
|
95
|
+
|
92
96
|
redacted
|
93
97
|
end
|
94
98
|
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "loggable"
|
4
|
+
|
5
|
+
module SchwabMCP
|
6
|
+
module SchwabClientFactory
|
7
|
+
extend Loggable
|
8
|
+
|
9
|
+
def self.create_client
|
10
|
+
begin
|
11
|
+
log_debug("Initializing Schwab client")
|
12
|
+
client = SchwabRb::Auth.init_client_easy(
|
13
|
+
ENV['SCHWAB_API_KEY'],
|
14
|
+
ENV['SCHWAB_APP_SECRET'],
|
15
|
+
ENV['SCHWAB_CALLBACK_URI'],
|
16
|
+
ENV['TOKEN_PATH']
|
17
|
+
)
|
18
|
+
|
19
|
+
unless client
|
20
|
+
log_error("Failed to initialize Schwab client - check credentials")
|
21
|
+
return nil
|
22
|
+
end
|
23
|
+
|
24
|
+
log_debug("Schwab client initialized successfully")
|
25
|
+
client
|
26
|
+
rescue => e
|
27
|
+
log_error("Error initializing Schwab client: #{e.message}")
|
28
|
+
log_debug("Backtrace: #{e.backtrace.first(3).join('\n')}")
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.cached_client
|
34
|
+
@client ||= create_client
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.client_error_response
|
38
|
+
MCP::Tool::Response.new([{
|
39
|
+
type: "text",
|
40
|
+
text: "**Error**: Failed to initialize Schwab client. Check your credentials."
|
41
|
+
}])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|