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.
- checksums.yaml +4 -4
- data/.claude/settings.json +14 -0
- data/CHANGELOG.md +12 -0
- data/CLAUDE.md +124 -0
- data/README.md +1 -7
- 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 +15 -4
- data/exe/schwab_token_refresh +12 -11
- data/exe/schwab_token_reset +11 -10
- 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 -81
- data/lib/schwab_mcp/tools/get_account_names_tool.rb +58 -0
- data/lib/schwab_mcp/tools/get_market_hours_tool.rb +27 -28
- data/lib/schwab_mcp/tools/get_order_tool.rb +50 -137
- data/lib/schwab_mcp/tools/get_price_history_tool.rb +23 -35
- data/lib/schwab_mcp/tools/help_tool.rb +12 -33
- data/lib/schwab_mcp/tools/list_account_orders_tool.rb +36 -90
- data/lib/schwab_mcp/tools/list_account_transactions_tool.rb +43 -98
- data/lib/schwab_mcp/tools/list_movers_tool.rb +21 -34
- data/lib/schwab_mcp/tools/option_chain_tool.rb +132 -84
- data/lib/schwab_mcp/tools/place_order_tool.rb +111 -141
- data/lib/schwab_mcp/tools/preview_order_tool.rb +71 -81
- data/lib/schwab_mcp/tools/quote_tool.rb +33 -28
- data/lib/schwab_mcp/tools/quotes_tool.rb +97 -45
- data/lib/schwab_mcp/tools/replace_order_tool.rb +110 -140
- data/lib/schwab_mcp/tools/schwab_account_details_tool.rb +56 -98
- data/lib/schwab_mcp/version.rb +1 -1
- data/lib/schwab_mcp.rb +11 -10
- metadata +12 -9
- 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/list_schwab_accounts_tool.rb +0 -162
- data/lib/schwab_mcp/tools/option_strategy_finder_tool.rb +0 -378
- 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 =
|
72
|
-
|
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
|
-
|
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
|
149
|
+
if price_history
|
162
150
|
log_info("Successfully retrieved price history for #{symbol}")
|
163
151
|
|
164
|
-
|
165
|
-
data
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
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
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
rescue
|
179
|
-
|
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
|
-
#
|
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
|
60
|
-
- **
|
61
|
-
- **
|
62
|
-
- **
|
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 (
|
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
|
-
#
|
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
|
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
|
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
|
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
|
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
|
-
#
|
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 =
|
87
|
-
|
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
|
-
|
136
|
-
|
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
|
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("
|
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
|
-
|
175
|
-
|
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
|
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(
|
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(
|
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
|
-
|
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: #{
|
189
|
+
formatted += "- Total Orders: #{orders_array.length}\n\n"
|
245
190
|
|
246
|
-
if
|
191
|
+
if orders_array.length > 0
|
247
192
|
formatted += "**Order Details:**\n"
|
248
|
-
|
193
|
+
orders_array.each_with_index do |order, index|
|
249
194
|
formatted += format_single_order(order, index + 1)
|
250
|
-
formatted += "\n" unless index ==
|
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
|
-
|
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
|
265
|
-
formatted += "- Status: #{order
|
266
|
-
formatted += "- Order Type: #{order
|
267
|
-
formatted += "-
|
268
|
-
formatted += "-
|
269
|
-
formatted += "-
|
270
|
-
formatted += "-
|
271
|
-
formatted += "- Quantity: #{order
|
272
|
-
formatted += "-
|
273
|
-
|
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
|
278
|
-
if leg
|
279
|
-
symbol = leg
|
280
|
-
instruction = leg
|
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 =
|
83
|
-
|
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
|
-
|
108
|
-
|
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**:
|
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
|
-
|
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
|
-
|
171
|
-
|
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
|
134
|
+
if transactions
|
179
135
|
log_info("Successfully retrieved transactions for #{account_name}")
|
180
|
-
|
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
|
-
|
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: #{
|
161
|
+
text: "**Error**: Failed to parse API response: #{err_msg}"
|
206
162
|
}])
|
207
163
|
rescue => e
|
208
|
-
|
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}: #{
|
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(
|
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
|
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(
|
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
|
264
|
-
formatted += "- Type: #{transaction
|
265
|
-
formatted += "- Status: #{transaction
|
266
|
-
formatted += "- Trade Date: #{transaction
|
267
|
-
formatted += "-
|
268
|
-
formatted += "-
|
269
|
-
formatted += "-
|
270
|
-
formatted += "-
|
271
|
-
|
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
|
231
|
+
transaction.transfer_items.each_with_index do |item, i|
|
276
232
|
formatted += " * Item #{i + 1}:\n"
|
277
|
-
formatted += " - Amount: $#{format_currency(item
|
278
|
-
formatted += " - Cost: $#{format_currency(item
|
279
|
-
formatted += " -
|
280
|
-
formatted += " -
|
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
|
284
|
-
instrument = item['instrument']
|
238
|
+
if item.instrument
|
285
239
|
formatted += " - Instrument:\n"
|
286
|
-
formatted += " * Symbol: #{instrument
|
287
|
-
formatted += " * Asset Type: #{instrument
|
288
|
-
formatted += " * Description: #{instrument
|
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
|