schwab_mcp 0.2.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/CHANGELOG.md +12 -0
- data/README.md +1 -7
- data/debug_env.rb +2 -2
- data/exe/schwab_mcp +5 -5
- data/exe/schwab_token_refresh +2 -2
- data/exe/schwab_token_reset +11 -10
- data/lib/schwab_mcp/schwab_client_factory.rb +1 -1
- data/lib/schwab_mcp/tools/cancel_order_tool.rb +7 -38
- data/lib/schwab_mcp/tools/get_account_names_tool.rb +58 -0
- data/lib/schwab_mcp/tools/get_order_tool.rb +6 -36
- data/lib/schwab_mcp/tools/help_tool.rb +11 -11
- data/lib/schwab_mcp/tools/list_account_orders_tool.rb +6 -32
- data/lib/schwab_mcp/tools/list_account_transactions_tool.rb +6 -32
- data/lib/schwab_mcp/tools/option_chain_tool.rb +3 -3
- data/lib/schwab_mcp/tools/place_order_tool.rb +34 -52
- data/lib/schwab_mcp/tools/preview_order_tool.rb +20 -82
- data/lib/schwab_mcp/tools/quote_tool.rb +3 -5
- data/lib/schwab_mcp/tools/replace_order_tool.rb +34 -52
- data/lib/schwab_mcp/tools/schwab_account_details_tool.rb +8 -34
- data/lib/schwab_mcp/version.rb +1 -1
- data/lib/schwab_mcp.rb +10 -8
- metadata +3 -10
- data/lib/schwab_mcp/tools/list_schwab_accounts_tool.rb +0 -149
- data/orders_example.json +0 -7084
- data/spx_option_chain.json +0 -25073
- data/start_mcp_server.sh +0 -4
- data/test_mcp.rb +0 -16
- data/test_server.rb +0 -23
- data/trading_brokerage_account_details.json +0 -89
- data/transactions_example.json +0 -488
@@ -19,7 +19,7 @@ module SchwabMCP
|
|
19
19
|
},
|
20
20
|
strategy_type: {
|
21
21
|
type: "string",
|
22
|
-
enum: %w[
|
22
|
+
enum: %w[SINGLE VERTICAL IRON_CONDOR],
|
23
23
|
description: "Type of options strategy to place"
|
24
24
|
},
|
25
25
|
price: {
|
@@ -101,11 +101,8 @@ module SchwabMCP
|
|
101
101
|
account_result = resolve_account_details(client, params[:account_name])
|
102
102
|
return account_result if account_result.is_a?(MCP::Tool::Response)
|
103
103
|
|
104
|
-
account_id, account_hash = account_result
|
105
|
-
|
106
104
|
order_builder = SchwabRb::Orders::OrderFactory.build(
|
107
105
|
strategy_type: params[:strategy_type],
|
108
|
-
account_number: account_id,
|
109
106
|
price: params[:price],
|
110
107
|
quantity: params[:quantity] || 1,
|
111
108
|
order_instruction: (params[:order_instruction] || "open").to_sym,
|
@@ -123,7 +120,7 @@ module SchwabMCP
|
|
123
120
|
)
|
124
121
|
|
125
122
|
log_debug("Making place order API request")
|
126
|
-
response = client.place_order(
|
123
|
+
response = client.place_order(account_name: params[:account_name], order: order_builder)
|
127
124
|
|
128
125
|
if response && (200..299).include?(response.status)
|
129
126
|
log_info("Successfully placed #{params[:strategy_type]} order (HTTP #{response.status})")
|
@@ -157,77 +154,62 @@ module SchwabMCP
|
|
157
154
|
end
|
158
155
|
|
159
156
|
def self.resolve_account_details(client, account_name)
|
160
|
-
|
161
|
-
unless
|
162
|
-
|
163
|
-
log_error("Account name '#{account_name}' not found in environment variables")
|
164
|
-
return MCP::Tool::Response.new([{
|
165
|
-
type: "text",
|
166
|
-
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."
|
167
|
-
}])
|
168
|
-
end
|
169
|
-
|
170
|
-
log_debug("Found account ID: [REDACTED] for account name: #{account_name}")
|
171
|
-
log_debug("Fetching account numbers mapping")
|
172
|
-
|
173
|
-
account_numbers = client.get_account_numbers
|
174
|
-
|
175
|
-
unless account_numbers
|
176
|
-
log_error("Failed to retrieve account numbers")
|
177
|
-
return MCP::Tool::Response.new([{
|
178
|
-
type: "text",
|
179
|
-
text: "**Error**: Failed to retrieve account numbers from Schwab API"
|
180
|
-
}])
|
181
|
-
end
|
182
|
-
|
183
|
-
log_debug("Account mappings retrieved (#{account_numbers.size} accounts found)")
|
184
|
-
|
185
|
-
account_hash = account_numbers.find_hash_value(account_id)
|
186
|
-
|
187
|
-
unless account_hash
|
188
|
-
log_error("Account ID not found in available accounts")
|
157
|
+
available_accounts = client.available_account_names
|
158
|
+
unless available_accounts.include?(account_name)
|
159
|
+
log_error("Account name '#{account_name}' not found in configured accounts")
|
189
160
|
return MCP::Tool::Response.new([{
|
190
161
|
type: "text",
|
191
|
-
text: "**Error**: Account
|
162
|
+
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."
|
192
163
|
}])
|
193
164
|
end
|
194
165
|
|
195
|
-
log_debug("
|
196
|
-
|
166
|
+
log_debug("Using account name: #{account_name}")
|
167
|
+
account_name
|
197
168
|
end
|
198
169
|
|
199
170
|
def self.validate_strategy_params(params)
|
200
|
-
|
201
|
-
|
171
|
+
strategy = params[:strategy_type].to_s.upcase
|
172
|
+
case strategy
|
173
|
+
when 'IRON_CONDOR'
|
202
174
|
required_fields = %i[put_short_symbol put_long_symbol call_short_symbol call_long_symbol]
|
203
175
|
missing_fields = required_fields.select { |field| params[field].nil? || params[field].empty? }
|
204
176
|
unless missing_fields.empty?
|
205
177
|
raise ArgumentError, "Iron condor strategy requires: #{missing_fields.join(", ")}"
|
206
178
|
end
|
207
|
-
when
|
179
|
+
when 'VERTICAL'
|
208
180
|
required_fields = %i[short_leg_symbol long_leg_symbol]
|
209
181
|
missing_fields = required_fields.select { |field| params[field].nil? || params[field].empty? }
|
210
182
|
unless missing_fields.empty?
|
211
183
|
raise ArgumentError, "#{params[:strategy_type]} strategy requires: #{missing_fields.join(", ")}"
|
212
184
|
end
|
185
|
+
when 'SINGLE'
|
186
|
+
required_fields = %i[symbol]
|
187
|
+
missing_fields = required_fields.select { |field| params[field].nil? || params[field].empty? }
|
188
|
+
unless missing_fields.empty?
|
189
|
+
raise ArgumentError, "#{params[:strategy_type]} strategy requires: #{missing_fields.join(", ")}"
|
190
|
+
end
|
213
191
|
else
|
214
192
|
raise ArgumentError, "Unsupported strategy type: #{params[:strategy_type]}"
|
215
193
|
end
|
216
194
|
end
|
217
195
|
|
218
196
|
def self.format_place_order_response(response, params)
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
197
|
+
strategy = params[:strategy_type].to_s.upcase
|
198
|
+
strategy_summary = case strategy
|
199
|
+
when 'IRON_CONDOR'
|
200
|
+
"**Iron Condor Order Placed**\n" \
|
201
|
+
"- Put Short: #{params[:put_short_symbol]}\n" \
|
202
|
+
"- Put Long: #{params[:put_long_symbol]}\n" \
|
203
|
+
"- Call Short: #{params[:call_short_symbol]}\n" \
|
204
|
+
"- Call Long: #{params[:call_long_symbol]}\n"
|
205
|
+
when 'VERTICAL'
|
206
|
+
"**Vertical Spread Order Placed**\n" \
|
207
|
+
"- Short Leg: #{params[:short_leg_symbol]}\n" \
|
208
|
+
"- Long Leg: #{params[:long_leg_symbol]}\n"
|
209
|
+
when 'SINGLE'
|
210
|
+
"**Single Option Order Placed**\n" \
|
211
|
+
"- Symbol: #{params[:symbol]}\n"
|
212
|
+
end
|
231
213
|
|
232
214
|
friendly_name = params[:account_name].gsub("_ACCOUNT", "").split("_").map(&:capitalize).join(" ")
|
233
215
|
|
@@ -20,7 +20,7 @@ module SchwabMCP
|
|
20
20
|
},
|
21
21
|
strategy_type: {
|
22
22
|
type: "string",
|
23
|
-
enum: [
|
23
|
+
enum: %w[SINGLE VERTICAL IRON_CONDOR],
|
24
24
|
description: "Type of options strategy to preview"
|
25
25
|
},
|
26
26
|
price: {
|
@@ -106,11 +106,8 @@ module SchwabMCP
|
|
106
106
|
account_result = resolve_account_details(client, params[:account_name])
|
107
107
|
return account_result if account_result.is_a?(MCP::Tool::Response)
|
108
108
|
|
109
|
-
account_id, account_hash = account_result
|
110
|
-
|
111
109
|
order_builder = SchwabRb::Orders::OrderFactory.build(
|
112
110
|
strategy_type: params[:strategy_type],
|
113
|
-
account_number: account_id,
|
114
111
|
price: params[:price],
|
115
112
|
quantity: params[:quantity] || 1,
|
116
113
|
order_instruction: (params[:order_instruction] || "open").to_sym,
|
@@ -124,11 +121,11 @@ module SchwabMCP
|
|
124
121
|
short_leg_symbol: params[:short_leg_symbol],
|
125
122
|
long_leg_symbol: params[:long_leg_symbol],
|
126
123
|
# Single option params
|
127
|
-
symbol: params[:symbol]
|
124
|
+
symbol: params[:symbol]
|
128
125
|
)
|
129
126
|
|
130
127
|
log_debug("Making preview order API request")
|
131
|
-
response = client.preview_order(
|
128
|
+
response = client.preview_order(account_name: params[:account_name], order: order_builder, return_data_objects: true)
|
132
129
|
|
133
130
|
if response
|
134
131
|
log_info("Successfully previewed #{params[:strategy_type]} order")
|
@@ -159,59 +156,36 @@ module SchwabMCP
|
|
159
156
|
private
|
160
157
|
|
161
158
|
def self.resolve_account_details(client, account_name)
|
162
|
-
|
163
|
-
unless
|
164
|
-
|
165
|
-
|
166
|
-
error_msg = "**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."
|
167
|
-
return MCP::Tool::Response.new([{
|
168
|
-
type: "text",
|
169
|
-
text: Redactor.redact_formatted_text(error_msg)
|
170
|
-
}])
|
171
|
-
end
|
172
|
-
|
173
|
-
log_debug("Found account ID: [REDACTED] for account name: #{account_name}")
|
174
|
-
log_debug("Fetching account numbers mapping")
|
175
|
-
|
176
|
-
account_numbers = client.get_account_numbers(return_data_objects: true)
|
177
|
-
unless account_numbers && !account_numbers.empty?
|
178
|
-
log_error("Failed to retrieve account numbers or no accounts returned")
|
179
|
-
error_msg = "**Error**: Failed to retrieve account numbers from Schwab API or no accounts returned"
|
180
|
-
return MCP::Tool::Response.new([{
|
181
|
-
type: "text",
|
182
|
-
text: Redactor.redact_formatted_text(error_msg)
|
183
|
-
}])
|
184
|
-
end
|
185
|
-
|
186
|
-
mapping = account_numbers.accounts.find { |acct| acct.account_number == account_id }
|
187
|
-
unless mapping
|
188
|
-
log_error("Account ID not found in available accounts")
|
189
|
-
error_msg = "**Error**: Account ID not found in available accounts. #{account_numbers.size} accounts available."
|
159
|
+
available_accounts = client.available_account_names
|
160
|
+
unless available_accounts.include?(account_name)
|
161
|
+
log_error("Account name '#{account_name}' not found in configured accounts")
|
162
|
+
error_msg = "**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."
|
190
163
|
return MCP::Tool::Response.new([{
|
191
164
|
type: "text",
|
192
165
|
text: Redactor.redact_formatted_text(error_msg)
|
193
166
|
}])
|
194
167
|
end
|
195
168
|
|
196
|
-
log_debug("
|
197
|
-
|
169
|
+
log_debug("Using account name: #{account_name}")
|
170
|
+
account_name
|
198
171
|
end
|
199
172
|
|
200
173
|
def self.validate_strategy_params(params)
|
201
|
-
|
202
|
-
|
174
|
+
strategy = params[:strategy_type].to_s.upcase
|
175
|
+
case strategy
|
176
|
+
when 'IRON_CONDOR'
|
203
177
|
required_fields = [:put_short_symbol, :put_long_symbol, :call_short_symbol, :call_long_symbol]
|
204
178
|
missing_fields = required_fields.select { |field| params[field].nil? || params[field].empty? }
|
205
179
|
unless missing_fields.empty?
|
206
180
|
raise ArgumentError, "Iron condor strategy requires: #{missing_fields.join(', ')}"
|
207
181
|
end
|
208
|
-
when '
|
182
|
+
when 'VERTICAL'
|
209
183
|
required_fields = [:short_leg_symbol, :long_leg_symbol]
|
210
184
|
missing_fields = required_fields.select { |field| params[field].nil? || params[field].empty? }
|
211
185
|
unless missing_fields.empty?
|
212
186
|
raise ArgumentError, "#{params[:strategy_type]} strategy requires: #{missing_fields.join(', ')}"
|
213
187
|
end
|
214
|
-
when '
|
188
|
+
when 'SINGLE'
|
215
189
|
required_fields = [:symbol]
|
216
190
|
missing_fields = required_fields.select { |field| params[field].nil? || params[field].empty? }
|
217
191
|
unless missing_fields.empty?
|
@@ -222,58 +196,22 @@ module SchwabMCP
|
|
222
196
|
end
|
223
197
|
end
|
224
198
|
|
225
|
-
def self.format_preview_response(response_body, params)
|
226
|
-
parsed = JSON.parse(response_body)
|
227
|
-
redacted_data = Redactor.redact(parsed)
|
228
|
-
|
229
|
-
begin
|
230
|
-
strategy_summary = case params[:strategy_type]
|
231
|
-
when 'ironcondor'
|
232
|
-
"**Iron Condor Preview**\n" \
|
233
|
-
"- Put Short: #{params[:put_short_symbol]}\n" \
|
234
|
-
"- Put Long: #{params[:put_long_symbol]}\n" \
|
235
|
-
"- Call Short: #{params[:call_short_symbol]}\n" \
|
236
|
-
"- Call Long: #{params[:call_long_symbol]}\n"
|
237
|
-
when 'vertical'
|
238
|
-
"**Vertical Preview**\n" \
|
239
|
-
"- Short Leg: #{params[:short_leg_symbol]}\n" \
|
240
|
-
"- Long Leg: #{params[:long_leg_symbol]}\n"
|
241
|
-
when 'single'
|
242
|
-
"**Single Option Preview**\n" \
|
243
|
-
"- Symbol: #{params[:symbol]}\n"
|
244
|
-
end
|
245
|
-
|
246
|
-
friendly_name = params[:account_name].gsub('_ACCOUNT', '').split('_').map(&:capitalize).join(' ')
|
247
|
-
|
248
|
-
order_details = "**Order Details:**\n" \
|
249
|
-
"- Strategy: #{params[:strategy_type]}\n" \
|
250
|
-
"- Action: #{params[:order_instruction] || 'open'}\n" \
|
251
|
-
"- Quantity: #{params[:quantity] || 1}\n" \
|
252
|
-
"- Price: $#{params[:price]}\n" \
|
253
|
-
"- Account: #{friendly_name} (#{params[:account_name]})\n\n"
|
254
|
-
|
255
|
-
full_response = "**Schwab API Preview Response:**\n\n```json\n#{JSON.pretty_generate(redacted_data)}\n```"
|
256
|
-
|
257
|
-
"#{strategy_summary}\n#{order_details}#{full_response}"
|
258
|
-
rescue JSON::ParserError
|
259
|
-
"**Order Preview Response:**\n\n```\n#{JSON.pretty_generate(redacted_data)}\n```"
|
260
|
-
end
|
261
|
-
end
|
262
199
|
def self.format_preview_response(order_preview, params)
|
263
200
|
# order_preview is a SchwabRb::DataObjects::OrderPreview
|
264
201
|
begin
|
265
|
-
|
266
|
-
|
202
|
+
strategy = params[:strategy_type].to_s.upcase
|
203
|
+
strategy_summary = case strategy
|
204
|
+
when 'IRON_CONDOR'
|
267
205
|
"**Iron Condor Preview**\n" \
|
268
206
|
"- Put Short: #{params[:put_short_symbol]}\n" \
|
269
207
|
"- Put Long: #{params[:put_long_symbol]}\n" \
|
270
208
|
"- Call Short: #{params[:call_short_symbol]}\n" \
|
271
209
|
"- Call Long: #{params[:call_long_symbol]}\n"
|
272
|
-
when '
|
273
|
-
"**Vertical Preview**\n" \
|
210
|
+
when 'VERTICAL'
|
211
|
+
"**Vertical Spread Preview**\n" \
|
274
212
|
"- Short Leg: #{params[:short_leg_symbol]}\n" \
|
275
213
|
"- Long Leg: #{params[:long_leg_symbol]}\n"
|
276
|
-
when '
|
214
|
+
when 'SINGLE'
|
277
215
|
"**Single Option Preview**\n" \
|
278
216
|
"- Symbol: #{params[:symbol]}\n"
|
279
217
|
end
|
@@ -16,8 +16,8 @@ module SchwabMCP
|
|
16
16
|
properties: {
|
17
17
|
symbol: {
|
18
18
|
type: "string",
|
19
|
-
description: "Instrument symbol (e.g., 'AAPL', 'TSLA')",
|
20
|
-
pattern:
|
19
|
+
description: "Instrument symbol (e.g., 'AAPL', 'TSLA', '$SPX')",
|
20
|
+
pattern: '^[\$\^]?[A-Za-z0-9]{1,5}$'
|
21
21
|
}
|
22
22
|
},
|
23
23
|
required: ["symbol"]
|
@@ -38,7 +38,7 @@ module SchwabMCP
|
|
38
38
|
return SchwabClientFactory.client_error_response unless client
|
39
39
|
|
40
40
|
log_debug("Making API request for symbol: #{symbol}")
|
41
|
-
quote_obj = client.get_quote(symbol.upcase
|
41
|
+
quote_obj = client.get_quote(symbol.upcase)
|
42
42
|
|
43
43
|
unless quote_obj
|
44
44
|
log_warn("No quote data object returned for symbol: #{symbol}")
|
@@ -48,7 +48,6 @@ module SchwabMCP
|
|
48
48
|
}])
|
49
49
|
end
|
50
50
|
|
51
|
-
# Format output based on quote type
|
52
51
|
formatted = format_quote_object(quote_obj)
|
53
52
|
log_info("Successfully retrieved quote for #{symbol}")
|
54
53
|
MCP::Tool::Response.new([{
|
@@ -66,7 +65,6 @@ module SchwabMCP
|
|
66
65
|
end
|
67
66
|
end
|
68
67
|
|
69
|
-
# Format the quote object for display
|
70
68
|
def self.format_quote_object(obj)
|
71
69
|
case obj
|
72
70
|
when SchwabRb::DataObjects::OptionQuote
|
@@ -23,7 +23,7 @@ module SchwabMCP
|
|
23
23
|
},
|
24
24
|
strategy_type: {
|
25
25
|
type: "string",
|
26
|
-
enum: %w[
|
26
|
+
enum: %w[SINGLE VERTICAL IRON_CONDOR],
|
27
27
|
description: "Type of options strategy for the replacement order"
|
28
28
|
},
|
29
29
|
price: {
|
@@ -105,11 +105,8 @@ module SchwabMCP
|
|
105
105
|
account_result = resolve_account_details(client, params[:account_name])
|
106
106
|
return account_result if account_result.is_a?(MCP::Tool::Response)
|
107
107
|
|
108
|
-
account_id, account_hash = account_result
|
109
|
-
|
110
108
|
order_builder = SchwabRb::Orders::OrderFactory.build(
|
111
109
|
strategy_type: params[:strategy_type],
|
112
|
-
account_number: account_id,
|
113
110
|
price: params[:price],
|
114
111
|
quantity: params[:quantity] || 1,
|
115
112
|
order_instruction: (params[:order_instruction] || "open").to_sym,
|
@@ -127,7 +124,7 @@ module SchwabMCP
|
|
127
124
|
)
|
128
125
|
|
129
126
|
log_debug("Making replace order API request for order ID: #{params[:order_id]}")
|
130
|
-
response = client.replace_order(
|
127
|
+
response = client.replace_order(account_name: params[:account_name], order_id: params[:order_id], order: order_builder)
|
131
128
|
|
132
129
|
if response && (200..299).include?(response.status)
|
133
130
|
log_info("Successfully replaced order #{params[:order_id]} with #{params[:strategy_type]} order (HTTP #{response.status})")
|
@@ -161,77 +158,62 @@ module SchwabMCP
|
|
161
158
|
end
|
162
159
|
|
163
160
|
def self.resolve_account_details(client, account_name)
|
164
|
-
|
165
|
-
unless
|
166
|
-
|
167
|
-
log_error("Account name '#{account_name}' not found in environment variables")
|
168
|
-
return MCP::Tool::Response.new([{
|
169
|
-
type: "text",
|
170
|
-
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."
|
171
|
-
}])
|
172
|
-
end
|
173
|
-
|
174
|
-
log_debug("Found account ID: [REDACTED] for account name: #{account_name}")
|
175
|
-
log_debug("Fetching account numbers mapping")
|
176
|
-
|
177
|
-
account_numbers = client.get_account_numbers
|
178
|
-
|
179
|
-
unless account_numbers
|
180
|
-
log_error("Failed to retrieve account numbers")
|
181
|
-
return MCP::Tool::Response.new([{
|
182
|
-
type: "text",
|
183
|
-
text: "**Error**: Failed to retrieve account numbers from Schwab API"
|
184
|
-
}])
|
185
|
-
end
|
186
|
-
|
187
|
-
log_debug("Account mappings retrieved (#{account_numbers.size} accounts found)")
|
188
|
-
|
189
|
-
account_hash = account_numbers.find_hash_value(account_id)
|
190
|
-
|
191
|
-
unless account_hash
|
192
|
-
log_error("Account ID not found in available accounts")
|
161
|
+
available_accounts = client.available_account_names
|
162
|
+
unless available_accounts.include?(account_name)
|
163
|
+
log_error("Account name '#{account_name}' not found in configured accounts")
|
193
164
|
return MCP::Tool::Response.new([{
|
194
165
|
type: "text",
|
195
|
-
text: "**Error**: Account
|
166
|
+
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."
|
196
167
|
}])
|
197
168
|
end
|
198
169
|
|
199
|
-
log_debug("
|
200
|
-
|
170
|
+
log_debug("Using account name: #{account_name}")
|
171
|
+
account_name
|
201
172
|
end
|
202
173
|
|
203
174
|
def self.validate_strategy_params(params)
|
204
|
-
|
205
|
-
|
175
|
+
strategy = params[:strategy_type].to_s.upcase
|
176
|
+
case strategy
|
177
|
+
when 'IRON_CONDOR'
|
206
178
|
required_fields = %i[put_short_symbol put_long_symbol call_short_symbol call_long_symbol]
|
207
179
|
missing_fields = required_fields.select { |field| params[field].nil? || params[field].empty? }
|
208
180
|
unless missing_fields.empty?
|
209
181
|
raise ArgumentError, "Iron condor strategy requires: #{missing_fields.join(", ")}"
|
210
182
|
end
|
211
|
-
when
|
183
|
+
when 'VERTICAL'
|
212
184
|
required_fields = %i[short_leg_symbol long_leg_symbol]
|
213
185
|
missing_fields = required_fields.select { |field| params[field].nil? || params[field].empty? }
|
214
186
|
unless missing_fields.empty?
|
215
187
|
raise ArgumentError, "#{params[:strategy_type]} strategy requires: #{missing_fields.join(", ")}"
|
216
188
|
end
|
189
|
+
when 'SINGLE'
|
190
|
+
required_fields = %i[symbol]
|
191
|
+
missing_fields = required_fields.select { |field| params[field].nil? || params[field].empty? }
|
192
|
+
unless missing_fields.empty?
|
193
|
+
raise ArgumentError, "#{params[:strategy_type]} strategy requires: #{missing_fields.join(", ")}"
|
194
|
+
end
|
217
195
|
else
|
218
196
|
raise ArgumentError, "Unsupported strategy type: #{params[:strategy_type]}"
|
219
197
|
end
|
220
198
|
end
|
221
199
|
|
222
200
|
def self.format_replace_order_response(response, params)
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
201
|
+
strategy = params[:strategy_type].to_s.upcase
|
202
|
+
strategy_summary = case strategy
|
203
|
+
when 'IRON_CONDOR'
|
204
|
+
"**Iron Condor Order Replaced**\n" \
|
205
|
+
"- Put Short: #{params[:put_short_symbol]}\n" \
|
206
|
+
"- Put Long: #{params[:put_long_symbol]}\n" \
|
207
|
+
"- Call Short: #{params[:call_short_symbol]}\n" \
|
208
|
+
"- Call Long: #{params[:call_long_symbol]}\n"
|
209
|
+
when 'VERTICAL'
|
210
|
+
"**Vertical Spread Order Replaced**\n" \
|
211
|
+
"- Short Leg: #{params[:short_leg_symbol]}\n" \
|
212
|
+
"- Long Leg: #{params[:long_leg_symbol]}\n"
|
213
|
+
when 'SINGLE'
|
214
|
+
"**Single Option Order Replaced**\n" \
|
215
|
+
"- Symbol: #{params[:symbol]}\n"
|
216
|
+
end
|
235
217
|
|
236
218
|
friendly_name = params[:account_name].gsub("_ACCOUNT", "").split("_").map(&:capitalize).join(" ")
|
237
219
|
|
@@ -51,50 +51,24 @@ module SchwabMCP
|
|
51
51
|
client = SchwabClientFactory.create_client
|
52
52
|
return SchwabClientFactory.client_error_response unless client
|
53
53
|
|
54
|
-
|
55
|
-
unless
|
56
|
-
|
57
|
-
log_error("Account name '#{account_name}' not found in environment variables")
|
54
|
+
available_accounts = client.available_account_names
|
55
|
+
unless available_accounts.include?(account_name)
|
56
|
+
log_error("Account name '#{account_name}' not found in configured accounts")
|
58
57
|
return MCP::Tool::Response.new([{
|
59
58
|
type: "text",
|
60
|
-
text: "**Error**: Account name '#{account_name}' not found in
|
59
|
+
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."
|
61
60
|
}])
|
62
61
|
end
|
63
62
|
|
64
|
-
log_debug("
|
65
|
-
log_debug("Fetching account numbers mapping")
|
66
|
-
|
67
|
-
account_numbers = client.get_account_numbers
|
68
|
-
|
69
|
-
unless account_numbers
|
70
|
-
log_error("Failed to retrieve account numbers")
|
71
|
-
return MCP::Tool::Response.new([{
|
72
|
-
type: "text",
|
73
|
-
text: "**Error**: Failed to retrieve account numbers from Schwab API"
|
74
|
-
}])
|
75
|
-
end
|
76
|
-
|
77
|
-
log_debug("Account numbers retrieved (#{account_numbers.size} accounts found)")
|
78
|
-
|
79
|
-
account_hash = account_numbers.find_hash_value(account_id)
|
80
|
-
|
81
|
-
unless account_hash
|
82
|
-
log_error("Account ID not found in available accounts")
|
83
|
-
return MCP::Tool::Response.new([{
|
84
|
-
type: "text",
|
85
|
-
text: "**Error**: Account ID not found in available accounts. #{account_numbers.size} accounts available."
|
86
|
-
}])
|
87
|
-
end
|
88
|
-
|
89
|
-
log_debug("Found account hash for account ID: #{account_name}")
|
63
|
+
log_debug("Using account name: #{account_name}")
|
90
64
|
log_debug("Fetching account information with fields: #{fields}")
|
91
65
|
|
92
|
-
account = client.get_account(
|
66
|
+
account = client.get_account(account_name: account_name, fields: fields)
|
93
67
|
|
94
68
|
if account
|
95
69
|
log_info("Successfully retrieved account information for #{account_name}")
|
96
70
|
|
97
|
-
formatted_response = format_account_data(account, account_name
|
71
|
+
formatted_response = format_account_data(account, account_name)
|
98
72
|
|
99
73
|
MCP::Tool::Response.new([{
|
100
74
|
type: "text",
|
@@ -126,7 +100,7 @@ module SchwabMCP
|
|
126
100
|
|
127
101
|
private
|
128
102
|
|
129
|
-
def self.format_account_data(account, account_name
|
103
|
+
def self.format_account_data(account, account_name)
|
130
104
|
friendly_name = account_name.gsub('_ACCOUNT', '').split('_').map(&:capitalize).join(' ')
|
131
105
|
|
132
106
|
formatted = "**Account Information for #{friendly_name} (#{account_name}):**\n\n"
|
data/lib/schwab_mcp/version.rb
CHANGED
data/lib/schwab_mcp.rb
CHANGED
@@ -11,7 +11,6 @@ require_relative "schwab_mcp/tools/quotes_tool"
|
|
11
11
|
require_relative "schwab_mcp/tools/option_chain_tool"
|
12
12
|
require_relative "schwab_mcp/tools/help_tool"
|
13
13
|
require_relative "schwab_mcp/tools/schwab_account_details_tool"
|
14
|
-
require_relative "schwab_mcp/tools/list_schwab_accounts_tool"
|
15
14
|
require_relative "schwab_mcp/tools/list_account_orders_tool"
|
16
15
|
require_relative "schwab_mcp/tools/list_account_transactions_tool"
|
17
16
|
require_relative "schwab_mcp/tools/get_order_tool"
|
@@ -22,6 +21,7 @@ require_relative "schwab_mcp/tools/replace_order_tool"
|
|
22
21
|
require_relative "schwab_mcp/tools/list_movers_tool"
|
23
22
|
require_relative "schwab_mcp/tools/get_market_hours_tool"
|
24
23
|
require_relative "schwab_mcp/tools/get_price_history_tool"
|
24
|
+
require_relative "schwab_mcp/tools/get_account_names_tool"
|
25
25
|
require_relative "schwab_mcp/loggable"
|
26
26
|
require_relative "schwab_mcp/schwab_client_factory"
|
27
27
|
|
@@ -35,7 +35,6 @@ module SchwabMCP
|
|
35
35
|
Tools::OptionChainTool,
|
36
36
|
Tools::HelpTool,
|
37
37
|
Tools::SchwabAccountDetailsTool,
|
38
|
-
Tools::ListSchwabAccountsTool,
|
39
38
|
Tools::ListAccountOrdersTool,
|
40
39
|
Tools::ListAccountTransactionsTool,
|
41
40
|
Tools::GetOrderTool,
|
@@ -45,35 +44,38 @@ module SchwabMCP
|
|
45
44
|
Tools::ReplaceOrderTool,
|
46
45
|
Tools::ListMoversTool,
|
47
46
|
Tools::GetMarketHoursTool,
|
48
|
-
Tools::GetPriceHistoryTool
|
47
|
+
Tools::GetPriceHistoryTool,
|
48
|
+
Tools::GetAccountNamesTool
|
49
49
|
].freeze
|
50
50
|
|
51
51
|
class Server
|
52
52
|
include Loggable
|
53
53
|
|
54
54
|
def initialize
|
55
|
-
|
55
|
+
configure_schwab_rb
|
56
56
|
|
57
57
|
@server = MCP::Server.new(
|
58
58
|
name: "schwab_mcp_server",
|
59
59
|
version: SchwabMCP::VERSION,
|
60
60
|
tools: TOOLS,
|
61
|
+
server_context: { user: "foobar" }
|
61
62
|
)
|
62
63
|
end
|
63
64
|
|
64
65
|
def start
|
65
66
|
configure_mcp
|
66
|
-
log_info("
|
67
|
-
log_info("
|
68
|
-
log_info("
|
67
|
+
log_info("Starting Schwab MCP Server #{SchwabMCP::VERSION}")
|
68
|
+
log_info("Available tools: #{TOOLS.map { |tool| tool.name.split('::').last }.join(', ')}")
|
69
|
+
log_info("Logs will be written to: #{log_file_path}")
|
69
70
|
transport = MCP::Transports::StdioTransport.new(@server)
|
70
71
|
transport.open
|
71
72
|
end
|
72
73
|
|
73
74
|
private
|
74
75
|
|
75
|
-
def
|
76
|
+
def configure_schwab_rb
|
76
77
|
SchwabRb.configure do |config|
|
78
|
+
config.schwab_home = ENV['SCHWAB_HOME']
|
77
79
|
config.logger = SchwabMCP::Logger.instance
|
78
80
|
config.log_level = ENV.fetch('LOG_LEVEL', 'INFO').upcase
|
79
81
|
end
|