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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/settings.json +14 -0
  3. data/CLAUDE.md +124 -0
  4. data/debug_env.rb +46 -0
  5. data/doc/DATA_OBJECTS_MIGRATION_TODO.md +80 -0
  6. data/doc/SCHWAB_CLIENT_FACTORY_REFACTOR_PLAN.md +187 -0
  7. data/exe/schwab_mcp +14 -3
  8. data/exe/schwab_token_refresh +10 -9
  9. data/lib/schwab_mcp/redactor.rb +4 -0
  10. data/lib/schwab_mcp/schwab_client_factory.rb +44 -0
  11. data/lib/schwab_mcp/tools/cancel_order_tool.rb +29 -50
  12. data/lib/schwab_mcp/tools/get_market_hours_tool.rb +27 -28
  13. data/lib/schwab_mcp/tools/get_order_tool.rb +51 -108
  14. data/lib/schwab_mcp/tools/get_price_history_tool.rb +23 -35
  15. data/lib/schwab_mcp/tools/help_tool.rb +1 -22
  16. data/lib/schwab_mcp/tools/list_account_orders_tool.rb +35 -63
  17. data/lib/schwab_mcp/tools/list_account_transactions_tool.rb +43 -72
  18. data/lib/schwab_mcp/tools/list_movers_tool.rb +21 -34
  19. data/lib/schwab_mcp/tools/list_schwab_accounts_tool.rb +18 -31
  20. data/lib/schwab_mcp/tools/option_chain_tool.rb +130 -82
  21. data/lib/schwab_mcp/tools/place_order_tool.rb +105 -117
  22. data/lib/schwab_mcp/tools/preview_order_tool.rb +100 -48
  23. data/lib/schwab_mcp/tools/quote_tool.rb +33 -26
  24. data/lib/schwab_mcp/tools/quotes_tool.rb +97 -45
  25. data/lib/schwab_mcp/tools/replace_order_tool.rb +104 -116
  26. data/lib/schwab_mcp/tools/schwab_account_details_tool.rb +56 -72
  27. data/lib/schwab_mcp/version.rb +1 -1
  28. data/lib/schwab_mcp.rb +1 -2
  29. data/orders_example.json +7084 -0
  30. data/spx_option_chain.json +25073 -0
  31. data/test_mcp.rb +16 -0
  32. data/test_server.rb +23 -0
  33. data/trading_brokerage_account_details.json +89 -0
  34. data/transactions_example.json +488 -0
  35. metadata +17 -7
  36. data/lib/schwab_mcp/option_chain_filter.rb +0 -213
  37. data/lib/schwab_mcp/orders/iron_condor_order.rb +0 -87
  38. data/lib/schwab_mcp/orders/order_factory.rb +0 -40
  39. data/lib/schwab_mcp/orders/vertical_order.rb +0 -62
  40. 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: c9da3afacad3bb1272bbff0e797dcaf882352d1263ab620446dd0c97d329debc
4
- data.tar.gz: 6ea0e964b8ab66f7d92fc88b766b5c4d3569d18ed2b016ae35053e0d0a11b159
3
+ metadata.gz: 7680eeeed2f4a22cf70f836af9ab019c60e6de76867408acf79d0ffdd3217b33
4
+ data.tar.gz: e236d0f991b031b84873fbc8244b623dd74603ac3ab043d3586a987a8656eb37
5
5
  SHA512:
6
- metadata.gz: 5000ea77ea70965802bf6b8bac785f01edb98b66568f3197678b5294aaa98a2f7e8e79912ca820307b06f40990ddfe6a24c2f51b24afbab947ee07c6ab909560
7
- data.tar.gz: 5515f8088908d5a8a5a5f6e104ab4e29d6024acf275791b854eb00cbf7029868a5ce59d3b55e6b7aec66835baf1d3112a4ccf5aafb13bfdcc08482887690c74c
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
- require "schwab_mcp"
4
- require "dotenv/load"
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
- SchwabMCP::Server.new.start
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
@@ -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 "Missing required environment variables: #{missing_vars.join(', ')}"
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
- SchwabRb::Auth.init_client_easy(
29
- ENV['SCHWAB_API_KEY'],
30
- ENV['SCHWAB_APP_SECRET'],
31
- ENV['SCHWAB_CALLBACK_URI'],
32
- token_path
33
- )
34
- puts "✅ Token refresh completed successfully"
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 "Token refresh failed: #{e.message}"
37
+ puts "Token refresh failed: #{e.message}"
37
38
  exit 1
38
39
  end
@@ -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