schwab_mcp 0.1.0 → 0.3.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/settings.json +14 -0
  3. data/CHANGELOG.md +12 -0
  4. data/CLAUDE.md +124 -0
  5. data/README.md +1 -7
  6. data/debug_env.rb +46 -0
  7. data/doc/DATA_OBJECTS_MIGRATION_TODO.md +80 -0
  8. data/doc/SCHWAB_CLIENT_FACTORY_REFACTOR_PLAN.md +187 -0
  9. data/exe/schwab_mcp +15 -4
  10. data/exe/schwab_token_refresh +12 -11
  11. data/exe/schwab_token_reset +11 -10
  12. data/lib/schwab_mcp/redactor.rb +4 -0
  13. data/lib/schwab_mcp/schwab_client_factory.rb +44 -0
  14. data/lib/schwab_mcp/tools/cancel_order_tool.rb +29 -81
  15. data/lib/schwab_mcp/tools/get_account_names_tool.rb +58 -0
  16. data/lib/schwab_mcp/tools/get_market_hours_tool.rb +27 -28
  17. data/lib/schwab_mcp/tools/get_order_tool.rb +50 -137
  18. data/lib/schwab_mcp/tools/get_price_history_tool.rb +23 -35
  19. data/lib/schwab_mcp/tools/help_tool.rb +12 -33
  20. data/lib/schwab_mcp/tools/list_account_orders_tool.rb +36 -90
  21. data/lib/schwab_mcp/tools/list_account_transactions_tool.rb +43 -98
  22. data/lib/schwab_mcp/tools/list_movers_tool.rb +21 -34
  23. data/lib/schwab_mcp/tools/option_chain_tool.rb +132 -84
  24. data/lib/schwab_mcp/tools/place_order_tool.rb +111 -141
  25. data/lib/schwab_mcp/tools/preview_order_tool.rb +71 -81
  26. data/lib/schwab_mcp/tools/quote_tool.rb +33 -28
  27. data/lib/schwab_mcp/tools/quotes_tool.rb +97 -45
  28. data/lib/schwab_mcp/tools/replace_order_tool.rb +110 -140
  29. data/lib/schwab_mcp/tools/schwab_account_details_tool.rb +56 -98
  30. data/lib/schwab_mcp/version.rb +1 -1
  31. data/lib/schwab_mcp.rb +11 -10
  32. metadata +12 -9
  33. data/lib/schwab_mcp/option_chain_filter.rb +0 -213
  34. data/lib/schwab_mcp/orders/iron_condor_order.rb +0 -87
  35. data/lib/schwab_mcp/orders/order_factory.rb +0 -40
  36. data/lib/schwab_mcp/orders/vertical_order.rb +0 -62
  37. data/lib/schwab_mcp/tools/list_schwab_accounts_tool.rb +0 -162
  38. data/lib/schwab_mcp/tools/option_strategy_finder_tool.rb +0 -378
  39. data/start_mcp_server.sh +0 -4
@@ -1,8 +1,8 @@
1
1
  require "mcp"
2
2
  require "schwab_rb"
3
- require "json"
4
3
  require "date"
5
4
  require_relative "../loggable"
5
+ require_relative "../schwab_client_factory"
6
6
 
7
7
  module SchwabMCP
8
8
  module Tools
@@ -68,20 +68,8 @@ module SchwabMCP
68
68
  log_info("Getting price history for symbol: #{symbol}")
69
69
 
70
70
  begin
71
- client = SchwabRb::Auth.init_client_easy(
72
- ENV['SCHWAB_API_KEY'],
73
- ENV['SCHWAB_APP_SECRET'],
74
- ENV['SCHWAB_CALLBACK_URI'],
75
- ENV['TOKEN_PATH']
76
- )
77
-
78
- unless client
79
- log_error("Failed to initialize Schwab client")
80
- return MCP::Tool::Response.new([{
81
- type: "text",
82
- text: "**Error**: Failed to initialize Schwab client. Check your credentials."
83
- }])
84
- end
71
+ client = SchwabClientFactory.create_client
72
+ return SchwabClientFactory.client_error_response unless client
85
73
 
86
74
  parsed_start = nil
87
75
  parsed_end = nil
@@ -146,7 +134,7 @@ module SchwabMCP
146
134
 
147
135
  log_debug("Making price history API request for symbol: #{symbol}")
148
136
 
149
- response = client.get_price_history(
137
+ price_history = client.get_price_history(
150
138
  symbol.upcase,
151
139
  period_type: period_type_enum,
152
140
  period: period,
@@ -158,29 +146,29 @@ module SchwabMCP
158
146
  need_previous_close: need_previous_close
159
147
  )
160
148
 
161
- if response&.body
149
+ if price_history
162
150
  log_info("Successfully retrieved price history for #{symbol}")
163
151
 
164
- begin
165
- data = JSON.parse(response.body)
166
- candles = data.dig("candles") || []
167
-
168
- summary = if candles.any?
169
- "Retrieved #{candles.length} price candles"
170
- else
171
- "No price data available for the specified parameters"
172
- end
152
+ summary = if price_history.empty?
153
+ "No price data available for the specified parameters"
154
+ else
155
+ "Retrieved #{price_history.count} price candles\n" \
156
+ "First candle: #{price_history.first_candle&.to_h}\n" \
157
+ "Last candle: #{price_history.last_candle&.to_h}"
158
+ end
173
159
 
174
- MCP::Tool::Response.new([{
175
- type: "text",
176
- text: "**Price History for #{symbol.upcase}:**\n\n#{summary}\n\n```json\n#{response.body}\n```"
177
- }])
178
- rescue JSON::ParserError
179
- MCP::Tool::Response.new([{
180
- type: "text",
181
- text: "**Price History for #{symbol.upcase}:**\n\n```json\n#{response.body}\n```"
182
- }])
160
+ # Show a compact JSON representation for advanced users
161
+ json_preview = begin
162
+ require "json"
163
+ JSON.pretty_generate(price_history.to_h)
164
+ rescue
165
+ price_history.to_h.inspect
183
166
  end
167
+
168
+ MCP::Tool::Response.new([{
169
+ type: "text",
170
+ text: "**Price History for #{symbol.upcase}:**\n\n#{summary}\n\n```json\n#{json_preview}\n```"
171
+ }])
184
172
  else
185
173
  log_warn("Empty response from Schwab API for symbol: #{symbol}")
186
174
  MCP::Tool::Response.new([{
@@ -44,7 +44,7 @@ module SchwabMCP
44
44
 
45
45
  def self.get_general_help
46
46
  <<~HELP
47
- # 📊 Schwab MCP Server
47
+ # Schwab MCP Server
48
48
 
49
49
  ## Available Tools:
50
50
 
@@ -56,11 +56,10 @@ module SchwabMCP
56
56
  - **get_market_hours_tool**: Get market hours for specified markets
57
57
  - **list_movers_tool**: Get top ten movers for a given index
58
58
 
59
- ### Option Strategy Tools:
60
- - **option_strategy_finder_tool**: Find option strategies (iron condor, call spread, put spread)
61
- - **preview_order_tool**: Preview an options order before placing (⚠️ SAFE PREVIEW)
62
- - **place_order_tool**: Place an options order for execution (⚠️ DESTRUCTIVE)
63
- - **replace_order_tool**: Replace an existing order with a new one (⚠️ DESTRUCTIVE)
59
+ ### Option Order Tools:
60
+ - **preview_order_tool**: Preview an options order before placing (SAFE PREVIEW)
61
+ - **place_order_tool**: Place an options order for execution (DESTRUCTIVE)
62
+ - **replace_order_tool**: Replace an existing order with a new one (DESTRUCTIVE)
64
63
 
65
64
  ### Account Management:
66
65
  - **schwab_account_details_tool**: Get account information using account name mapping
@@ -68,7 +67,7 @@ module SchwabMCP
68
67
  - **list_account_orders_tool**: List orders for a specific account using account name mapping
69
68
  - **list_account_transactions_tool**: List transactions for a specific account
70
69
  - **get_order_tool**: Get detailed information for a specific order by order ID
71
- - **cancel_order_tool**: Cancel a specific order by order ID (⚠️ DESTRUCTIVE)
70
+ - **cancel_order_tool**: Cancel a specific order by order ID (DESTRUCTIVE)
72
71
 
73
72
  ### Documentation:
74
73
  - **help_tool**: This help system
@@ -84,7 +83,6 @@ module SchwabMCP
84
83
 
85
84
  # Options
86
85
  option_chain_tool(symbol: "SPX", contract_type: "ALL")
87
- option_strategy_finder_tool(strategy_type: "ironcondor", underlying_symbol: "SPX", expiration_date: "2025-01-17")
88
86
  preview_order_tool(account_name: "TRADING_BROKERAGE_ACCOUNT", strategy_type: "ironcondor", price: 1.50, quantity: 1)
89
87
 
90
88
  # Account Management
@@ -120,7 +118,7 @@ module SchwabMCP
120
118
 
121
119
  def self.get_tools_help
122
120
  <<~HELP
123
- # 🔧 Available Tools
121
+ # Available Tools
124
122
 
125
123
  ## Market Data & Analysis Tools
126
124
 
@@ -197,28 +195,9 @@ module SchwabMCP
197
195
 
198
196
  **Example**: `option_chain_tool(symbol: "SPX", contract_type: "ALL")`
199
197
 
200
- ### option_strategy_finder_tool
201
- Find option strategies using sophisticated algorithms.
202
- **Parameters**:
203
- - `strategy_type` (required) - "ironcondor", "callspread", or "putspread"
204
- - `underlying_symbol` (required) - e.g., "SPX", "$SPX"
205
- - `expiration_date` (required) - Target expiration "YYYY-MM-DD"
206
- - `max_delta` (optional) - Maximum delta for short legs (default: 0.15)
207
- - `max_spread` (optional) - Maximum spread width (default: 20.0)
208
- - `min_credit` (optional) - Minimum credit in dollars (default: 100.0)
209
- - `min_open_interest` (optional) - Minimum open interest (default: 0)
210
- - `dist_from_strike` (optional) - Min distance from current price (default: 0.07)
211
- - `expiration_type`, `settlement_type`, `option_root` (optional) - Filters
212
-
213
- **Examples**:
214
- ```
215
- option_strategy_finder_tool(strategy_type: "ironcondor", underlying_symbol: "SPX", expiration_date: "2025-01-17")
216
- option_strategy_finder_tool(strategy_type: "callspread", underlying_symbol: "SPY", expiration_date: "2025-01-10", max_delta: 0.20, min_credit: 50.0)
217
- ```
218
-
219
198
  ## Order Management Tools
220
199
 
221
- ### preview_order_tool ⚠️ SAFE PREVIEW
200
+ ### preview_order_tool SAFE PREVIEW
222
201
  Preview an options order before placing to validate structure and see estimated costs.
223
202
  **Parameters**:
224
203
  - `account_name` (required) - Account name ending with '_ACCOUNT'
@@ -234,7 +213,7 @@ module SchwabMCP
234
213
  preview_order_tool(account_name: "TRADING_BROKERAGE_ACCOUNT", strategy_type: "callspread", short_symbol: "SPY250117C00600000", long_symbol: "SPY250117C00610000", price: 2.50)
235
214
  ```
236
215
 
237
- ### place_order_tool ⚠️ DESTRUCTIVE OPERATION
216
+ ### place_order_tool DESTRUCTIVE OPERATION
238
217
  Place an options order for execution. **WARNING**: This places real orders with real money.
239
218
  **Parameters**: Same as preview_order_tool
240
219
  **Safety Features**:
@@ -247,7 +226,7 @@ module SchwabMCP
247
226
  place_order_tool(account_name: "TRADING_BROKERAGE_ACCOUNT", strategy_type: "ironcondor", price: 1.50, quantity: 1)
248
227
  ```
249
228
 
250
- ### replace_order_tool ⚠️ DESTRUCTIVE OPERATION
229
+ ### replace_order_tool DESTRUCTIVE OPERATION
251
230
  Replace an existing order with a new one. **WARNING**: Cancels existing order and places new one.
252
231
  **Parameters**:
253
232
  - `order_id` (required) - ID of existing order to replace
@@ -319,7 +298,7 @@ module SchwabMCP
319
298
  get_order_tool(order_id: "987654321", account_name: "IRA_ACCOUNT")
320
299
  ```
321
300
 
322
- ### cancel_order_tool ⚠️ DESTRUCTIVE OPERATION
301
+ ### cancel_order_tool DESTRUCTIVE OPERATION
323
302
  Cancel a specific order by order ID. **WARNING**: This action cannot be undone.
324
303
  **Parameters**:
325
304
  - `order_id` (required) - The numeric order ID to cancel
@@ -356,7 +335,7 @@ module SchwabMCP
356
335
 
357
336
  def self.get_setup_help
358
337
  <<~HELP
359
- # 🚀 Setup Guide
338
+ # Setup Guide
360
339
 
361
340
  ## 1. Environment Variables
362
341
  Set these required environment variables:
@@ -4,6 +4,7 @@ require "json"
4
4
  require "date"
5
5
  require_relative "../loggable"
6
6
  require_relative "../redactor"
7
+ require_relative "../schwab_client_factory"
7
8
 
8
9
  module SchwabMCP
9
10
  module Tools
@@ -83,64 +84,19 @@ module SchwabMCP
83
84
  end
84
85
 
85
86
  begin
86
- client = SchwabRb::Auth.init_client_easy(
87
- ENV['SCHWAB_API_KEY'],
88
- ENV['SCHWAB_APP_SECRET'],
89
- ENV['SCHWAB_CALLBACK_URI'],
90
- ENV['TOKEN_PATH']
91
- )
92
-
93
- unless client
94
- log_error("Failed to initialize Schwab client")
95
- return MCP::Tool::Response.new([{
96
- type: "text",
97
- text: "**Error**: Failed to initialize Schwab client. Check your credentials."
98
- }])
99
- end
100
-
101
- account_id = ENV[account_name]
102
- unless account_id
103
- available_accounts = ENV.keys.select { |key| key.end_with?('_ACCOUNT') }
104
- log_error("Account name '#{account_name}' not found in environment variables")
105
- return MCP::Tool::Response.new([{
106
- type: "text",
107
- text: "**Error**: Account name '#{account_name}' not found in environment variables.\n\nAvailable accounts: #{available_accounts.join(', ')}\n\nTo configure: Set ENV['#{account_name}'] to your account ID."
108
- }])
109
- end
110
-
111
- log_debug("Found account ID: [REDACTED] for account name: #{account_name}")
112
- log_debug("Fetching account numbers mapping")
113
-
114
- account_numbers_response = client.get_account_numbers
115
-
116
- unless account_numbers_response&.body
117
- log_error("Failed to retrieve account numbers")
118
- return MCP::Tool::Response.new([{
119
- type: "text",
120
- text: "**Error**: Failed to retrieve account numbers from Schwab API"
121
- }])
122
- end
123
-
124
- account_mappings = JSON.parse(account_numbers_response.body, symbolize_names: true)
125
- log_debug("Account mappings retrieved (#{account_mappings.length} accounts found)")
126
-
127
- account_hash = nil
128
- account_mappings.each do |mapping|
129
- if mapping[:accountNumber] == account_id
130
- account_hash = mapping[:hashValue]
131
- break
132
- end
133
- end
87
+ client = SchwabClientFactory.create_client
88
+ return SchwabClientFactory.client_error_response unless client
134
89
 
135
- unless account_hash
136
- log_error("Account ID not found in available accounts")
90
+ available_accounts = client.available_account_names
91
+ unless available_accounts.include?(account_name)
92
+ log_error("Account name '#{account_name}' not found in configured accounts")
137
93
  return MCP::Tool::Response.new([{
138
94
  type: "text",
139
- text: "**Error**: Account ID not found in available accounts. #{account_mappings.length} accounts available."
95
+ text: "**Error**: Account name '#{account_name}' not found in configured accounts.\n\nAvailable accounts: #{available_accounts.join(', ')}\n\nTo configure: Add the account to your schwab_rb configuration file."
140
96
  }])
141
97
  end
142
98
 
143
- log_debug("Found account hash for account ID: #{account_name}")
99
+ log_debug("Using account name: #{account_name}")
144
100
 
145
101
  from_datetime = nil
146
102
  to_datetime = nil
@@ -171,19 +127,18 @@ module SchwabMCP
171
127
 
172
128
  log_debug("Fetching orders with params - max_results: #{max_results}, from_datetime: #{from_datetime}, to_datetime: #{to_datetime}, status: #{status}")
173
129
 
174
- orders_response = client.get_account_orders(
175
- account_hash,
130
+ orders = client.get_account_orders(
131
+ account_name: account_name,
176
132
  max_results: max_results,
177
133
  from_entered_datetime: from_datetime,
178
134
  to_entered_datetime: to_datetime,
179
135
  status: status
180
136
  )
181
137
 
182
- if orders_response&.body
138
+ if orders
183
139
  log_info("Successfully retrieved orders for #{account_name}")
184
- orders_data = JSON.parse(orders_response.body)
185
140
 
186
- formatted_response = format_orders_data(orders_data, account_name, {
141
+ formatted_response = format_orders_data(orders, account_name, {
187
142
  max_results: max_results,
188
143
  from_date: from_date,
189
144
  to_date: to_date,
@@ -202,12 +157,6 @@ module SchwabMCP
202
157
  }])
203
158
  end
204
159
 
205
- rescue JSON::ParserError => e
206
- log_error("JSON parsing error: #{e.message}")
207
- MCP::Tool::Response.new([{
208
- type: "text",
209
- text: "**Error**: Failed to parse API response: #{e.message}"
210
- }])
211
160
  rescue => e
212
161
  log_error("Error retrieving orders for #{account_name}: #{e.message}")
213
162
  log_debug("Backtrace: #{e.backtrace.first(3).join('\n')}")
@@ -220,7 +169,7 @@ module SchwabMCP
220
169
 
221
170
  private
222
171
 
223
- def self.format_orders_data(orders_data, account_name, filters)
172
+ def self.format_orders_data(orders, account_name, filters)
224
173
  friendly_name = account_name.gsub('_ACCOUNT', '').split('_').map(&:capitalize).join(' ')
225
174
 
226
175
  formatted = "**Orders for #{friendly_name} (#{account_name}):**\n\n"
@@ -234,26 +183,24 @@ module SchwabMCP
234
183
  formatted += "\n"
235
184
  end
236
185
 
237
- if orders_data.is_a?(Array)
238
- orders = orders_data
239
- else
240
- orders = [orders_data]
241
- end
186
+ orders_array = orders.is_a?(Array) ? orders : [orders]
242
187
 
243
188
  formatted += "**Orders Summary:**\n"
244
- formatted += "- Total Orders: #{orders.length}\n\n"
189
+ formatted += "- Total Orders: #{orders_array.length}\n\n"
245
190
 
246
- if orders.length > 0
191
+ if orders_array.length > 0
247
192
  formatted += "**Order Details:**\n"
248
- orders.each_with_index do |order, index|
193
+ orders_array.each_with_index do |order, index|
249
194
  formatted += format_single_order(order, index + 1)
250
- formatted += "\n" unless index == orders.length - 1
195
+ formatted += "\n" unless index == orders_array.length - 1
251
196
  end
252
197
  else
253
198
  formatted += "No orders found matching the specified criteria.\n"
254
199
  end
255
200
 
256
- redacted_data = Redactor.redact(orders_data)
201
+ # Convert data objects to hash for redaction and display
202
+ orders_hash = orders_array.map(&:to_h)
203
+ redacted_data = Redactor.redact(orders_hash)
257
204
  formatted += "\n**Full Response (Redacted):**\n"
258
205
  formatted += "```json\n#{JSON.pretty_generate(redacted_data)}\n```"
259
206
  formatted
@@ -261,23 +208,22 @@ module SchwabMCP
261
208
 
262
209
  def self.format_single_order(order, order_num)
263
210
  formatted = "**Order #{order_num}:**\n"
264
- formatted += "- Order ID: #{order['orderId']}\n" if order['orderId']
265
- formatted += "- Status: #{order['status']}\n" if order['status']
266
- formatted += "- Order Type: #{order['orderType']}\n" if order['orderType']
267
- formatted += "- Session: #{order['session']}\n" if order['session']
268
- formatted += "- Duration: #{order['duration']}\n" if order['duration']
269
- formatted += "- Entered Time: #{order['enteredTime']}\n" if order['enteredTime']
270
- formatted += "- Close Time: #{order['closeTime']}\n" if order['closeTime']
271
- formatted += "- Quantity: #{order['quantity']}\n" if order['quantity']
272
- formatted += "- Filled Quantity: #{order['filledQuantity']}\n" if order['filledQuantity']
273
- formatted += "- Price: $#{format_currency(order['price'])}\n" if order['price']
274
-
275
- if order['orderLegCollection'] && order['orderLegCollection'].any?
211
+ formatted += "- Order ID: #{order.order_id}\n" if order.order_id
212
+ formatted += "- Status: #{order.status}\n" if order.status
213
+ formatted += "- Order Type: #{order.order_type}\n" if order.order_type
214
+ formatted += "- Duration: #{order.duration}\n" if order.duration
215
+ formatted += "- Entered Time: #{order.entered_time}\n" if order.entered_time
216
+ formatted += "- Close Time: #{order.close_time}\n" if order.close_time
217
+ formatted += "- Quantity: #{order.quantity}\n" if order.quantity
218
+ formatted += "- Filled Quantity: #{order.filled_quantity}\n" if order.filled_quantity
219
+ formatted += "- Price: $#{format_currency(order.price)}\n" if order.price
220
+
221
+ if order.order_leg_collection && order.order_leg_collection.any?
276
222
  formatted += "- Instruments:\n"
277
- order['orderLegCollection'].each do |leg|
278
- if leg['instrument']
279
- symbol = leg['instrument']['symbol']
280
- instruction = leg['instruction']
223
+ order.order_leg_collection.each do |leg|
224
+ if leg.instrument
225
+ symbol = leg.instrument.symbol
226
+ instruction = leg.instruction
281
227
  formatted += " * #{symbol} - #{instruction}\n"
282
228
  end
283
229
  end
@@ -4,6 +4,7 @@ require "json"
4
4
  require "date"
5
5
  require_relative "../loggable"
6
6
  require_relative "../redactor"
7
+ require_relative "../schwab_client_factory"
7
8
 
8
9
  module SchwabMCP
9
10
  module Tools
@@ -79,64 +80,19 @@ module SchwabMCP
79
80
  end
80
81
 
81
82
  begin
82
- client = SchwabRb::Auth.init_client_easy(
83
- ENV['SCHWAB_API_KEY'],
84
- ENV['SCHWAB_APP_SECRET'],
85
- ENV['SCHWAB_CALLBACK_URI'],
86
- ENV['TOKEN_PATH']
87
- )
88
-
89
- unless client
90
- log_error("Failed to initialize Schwab client")
91
- return MCP::Tool::Response.new([{
92
- type: "text",
93
- text: "**Error**: Failed to initialize Schwab client. Check your credentials."
94
- }])
95
- end
96
-
97
- account_id = ENV[account_name]
98
- unless account_id
99
- available_accounts = ENV.keys.select { |key| key.end_with?('_ACCOUNT') }
100
- log_error("Account name '#{account_name}' not found in environment variables")
101
- return MCP::Tool::Response.new([{
102
- type: "text",
103
- text: "**Error**: Account name '#{account_name}' not found in environment variables.\n\nAvailable accounts: #{available_accounts.join(', ')}\n\nTo configure: Set ENV['#{account_name}'] to your account ID."
104
- }])
105
- end
83
+ client = SchwabClientFactory.create_client
84
+ return SchwabClientFactory.client_error_response unless client
106
85
 
107
- log_debug("Found account ID: [REDACTED] for account name: #{account_name}")
108
- log_debug("Fetching account numbers mapping")
109
-
110
- account_numbers_response = client.get_account_numbers
111
-
112
- unless account_numbers_response&.body
113
- log_error("Failed to retrieve account numbers")
86
+ available_accounts = client.available_account_names
87
+ unless available_accounts.include?(account_name)
88
+ log_error("Account name '#{account_name}' not found in configured accounts")
114
89
  return MCP::Tool::Response.new([{
115
90
  type: "text",
116
- text: "**Error**: Failed to retrieve account numbers from Schwab API"
91
+ text: "**Error**: Account name '#{account_name}' not found in configured accounts.\n\nAvailable accounts: #{available_accounts.join(', ')}\n\nTo configure: Add the account to your schwab_rb configuration file."
117
92
  }])
118
93
  end
119
94
 
120
- account_mappings = JSON.parse(account_numbers_response.body, symbolize_names: true)
121
- log_debug("Account mappings retrieved (#{account_mappings.length} accounts found)")
122
-
123
- account_hash = nil
124
- account_mappings.each do |mapping|
125
- if mapping[:accountNumber] == account_id
126
- account_hash = mapping[:hashValue]
127
- break
128
- end
129
- end
130
-
131
- unless account_hash
132
- log_error("Account ID not found in available accounts")
133
- return MCP::Tool::Response.new([{
134
- type: "text",
135
- text: "**Error**: Account ID not found in available accounts. #{account_mappings.length} accounts available."
136
- }])
137
- end
138
-
139
- log_debug("Found account hash for account ID: #{account_name}")
95
+ log_debug("Using account name: #{account_name}")
140
96
 
141
97
  start_date_obj = nil
142
98
  end_date_obj = nil
@@ -167,19 +123,17 @@ module SchwabMCP
167
123
 
168
124
  log_debug("Fetching transactions with params - start_date: #{start_date_obj}, end_date: #{end_date_obj}, transaction_types: #{transaction_types}, symbol: #{symbol}")
169
125
 
170
- transactions_response = client.get_transactions(
171
- account_hash,
126
+ transactions = client.get_transactions(
127
+ account_name: account_name,
172
128
  start_date: start_date_obj,
173
129
  end_date: end_date_obj,
174
130
  transaction_types: transaction_types,
175
131
  symbol: symbol
176
132
  )
177
133
 
178
- if transactions_response&.body
134
+ if transactions
179
135
  log_info("Successfully retrieved transactions for #{account_name}")
180
- transactions_data = JSON.parse(transactions_response.body)
181
-
182
- formatted_response = format_transactions_data(transactions_data, account_name, {
136
+ formatted_response = format_transactions_data(transactions, account_name, {
183
137
  start_date: start_date,
184
138
  end_date: end_date,
185
139
  transaction_types: transaction_types,
@@ -199,24 +153,29 @@ module SchwabMCP
199
153
  end
200
154
 
201
155
  rescue JSON::ParserError => e
202
- log_error("JSON parsing error: #{e.message}")
156
+ redactor = Redactor.new
157
+ err_msg = redactor.redact(e.message)
158
+ log_error("JSON parsing error: #{err_msg}")
203
159
  MCP::Tool::Response.new([{
204
160
  type: "text",
205
- text: "**Error**: Failed to parse API response: #{e.message}"
161
+ text: "**Error**: Failed to parse API response: #{err_msg}"
206
162
  }])
207
163
  rescue => e
208
- log_error("Error retrieving transactions for #{account_name}: #{e.message}")
164
+ redactor = Redactor.new
165
+ err_msg = redactor.redact(e.message)
166
+ log_error("Error retrieving transactions for #{account_name}: #{err_msg}")
209
167
  log_debug("Backtrace: #{e.backtrace.first(3).join('\n')}")
168
+
210
169
  MCP::Tool::Response.new([{
211
170
  type: "text",
212
- text: "**Error** retrieving transactions for #{account_name}: #{e.message}\n\n#{e.backtrace.first(3).join('\n')}"
171
+ text: "**Error** retrieving transactions for #{account_name}: #{err_msg}\n\n#{e.backtrace.first(3).join('\n')}"
213
172
  }])
214
173
  end
215
174
  end
216
175
 
217
176
  private
218
177
 
219
- def self.format_transactions_data(transactions_data, account_name, filters)
178
+ def self.format_transactions_data(transactions, account_name, filters)
220
179
  friendly_name = account_name.gsub('_ACCOUNT', '').split('_').map(&:capitalize).join(' ')
221
180
 
222
181
  formatted = "**Transactions for #{friendly_name} (#{account_name}):**\n\n"
@@ -230,13 +189,11 @@ module SchwabMCP
230
189
  formatted += "\n"
231
190
  end
232
191
 
233
- transactions = transactions_data.is_a?(Array) ? transactions_data : [transactions_data]
234
-
235
192
  formatted += "**Transactions Summary:**\n"
236
193
  formatted += "- Total Transactions: #{transactions.length}\n\n"
237
194
 
238
195
  if transactions.length > 0
239
- transactions_by_type = transactions.group_by { |t| t['type'] }
196
+ transactions_by_type = transactions.group_by { |t| t.type }
240
197
  formatted += "**Transactions by Type:**\n"
241
198
  transactions_by_type.each do |type, type_transactions|
242
199
  formatted += "- #{type}: #{type_transactions.length} transactions\n"
@@ -252,7 +209,7 @@ module SchwabMCP
252
209
  formatted += "No transactions found matching the specified criteria.\n"
253
210
  end
254
211
 
255
- redacted_data = Redactor.redact(transactions_data)
212
+ redacted_data = Redactor.redact(transactions.map(&:to_h))
256
213
  formatted += "\n**Full Response (Redacted):**\n"
257
214
  formatted += "```json\n#{JSON.pretty_generate(redacted_data)}\n```"
258
215
  formatted
@@ -260,41 +217,29 @@ module SchwabMCP
260
217
 
261
218
  def self.format_single_transaction(transaction, transaction_num)
262
219
  formatted = "**Transaction #{transaction_num}:**\n"
263
- formatted += "- Activity ID: #{transaction['activityId']}\n" if transaction['activityId']
264
- formatted += "- Type: #{transaction['type']}\n" if transaction['type']
265
- formatted += "- Status: #{transaction['status']}\n" if transaction['status']
266
- formatted += "- Trade Date: #{transaction['tradeDate']}\n" if transaction['tradeDate']
267
- formatted += "- Settlement Date: #{transaction['settlementDate']}\n" if transaction['settlementDate']
268
- formatted += "- Net Amount: $#{format_currency(transaction['netAmount'])}\n" if transaction['netAmount']
269
- formatted += "- Sub Account: #{transaction['subAccount']}\n" if transaction['subAccount']
270
- formatted += "- Order ID: #{transaction['orderId']}\n" if transaction['orderId']
271
- formatted += "- Position ID: #{transaction['positionId']}\n" if transaction['positionId']
272
-
273
- if transaction['transferItems'] && transaction['transferItems'].any?
220
+ formatted += "- Activity ID: #{transaction.activity_id}\n" if transaction.activity_id
221
+ formatted += "- Type: #{transaction.type}\n" if transaction.type
222
+ formatted += "- Status: #{transaction.status}\n" if transaction.status
223
+ formatted += "- Trade Date: #{transaction.trade_date}\n" if transaction.trade_date
224
+ formatted += "- Net Amount: $#{format_currency(transaction.net_amount)}\n" if transaction.net_amount
225
+ formatted += "- Sub Account: #{transaction.sub_account}\n" if transaction.sub_account
226
+ formatted += "- Order ID: #{transaction.order_id}\n" if transaction.order_id
227
+ formatted += "- Position ID: #{transaction.position_id}\n" if transaction.position_id
228
+
229
+ if transaction.transfer_items && transaction.transfer_items.any?
274
230
  formatted += "- Transfer Items:\n"
275
- transaction['transferItems'].each_with_index do |item, i|
231
+ transaction.transfer_items.each_with_index do |item, i|
276
232
  formatted += " * Item #{i + 1}:\n"
277
- formatted += " - Amount: $#{format_currency(item['amount'])}\n" if item['amount']
278
- formatted += " - Cost: $#{format_currency(item['cost'])}\n" if item['cost']
279
- formatted += " - Price: $#{format_currency(item['price'])}\n" if item['price']
280
- formatted += " - Fee Type: #{item['feeType']}\n" if item['feeType']
281
- formatted += " - Position Effect: #{item['positionEffect']}\n" if item['positionEffect']
233
+ formatted += " - Amount: $#{format_currency(item.amount)}\n" if item.amount
234
+ formatted += " - Cost: $#{format_currency(item.cost)}\n" if item.cost
235
+ formatted += " - Fee Type: #{item.fee_type}\n" if item.fee_type
236
+ formatted += " - Position Effect: #{item.position_effect}\n" if item.position_effect
282
237
 
283
- if item['instrument']
284
- instrument = item['instrument']
238
+ if item.instrument
285
239
  formatted += " - Instrument:\n"
286
- formatted += " * Symbol: #{instrument['symbol']}\n" if instrument['symbol']
287
- formatted += " * Asset Type: #{instrument['assetType']}\n" if instrument['assetType']
288
- formatted += " * Description: #{instrument['description']}\n" if instrument['description']
289
- formatted += " * Closing Price: $#{format_currency(instrument['closingPrice'])}\n" if instrument['closingPrice']
290
-
291
- # Options-specific fields
292
- if instrument['assetType'] == 'OPTION'
293
- formatted += " * Strike Price: $#{format_currency(instrument['strikePrice'])}\n" if instrument['strikePrice']
294
- formatted += " * Put/Call: #{instrument['putCall']}\n" if instrument['putCall']
295
- formatted += " * Expiration Date: #{instrument['expirationDate']}\n" if instrument['expirationDate']
296
- formatted += " * Underlying Symbol: #{instrument['underlyingSymbol']}\n" if instrument['underlyingSymbol']
297
- end
240
+ formatted += " * Symbol: #{item.instrument.symbol}\n" if item.instrument.symbol
241
+ formatted += " * Asset Type: #{item.instrument.respond_to?(:asset_type) ? item.instrument.asset_type : nil}\n" if item.instrument.respond_to?(:asset_type) && item.instrument.asset_type
242
+ formatted += " * Description: #{item.instrument.description}\n" if item.instrument.description
298
243
  end
299
244
  end
300
245
  end