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
@@ -3,6 +3,7 @@ require "schwab_rb"
3
3
  require "json"
4
4
  require_relative "../loggable"
5
5
  require_relative "../redactor"
6
+ require_relative "../schwab_client_factory"
6
7
 
7
8
  module SchwabMCP
8
9
  module Tools
@@ -19,7 +20,7 @@ module SchwabMCP
19
20
  },
20
21
  fields: {
21
22
  type: "array",
22
- description: "Optional account fields to retrieve (balances, positions, orders)",
23
+ description: "Optional account fields to retrieve (positions)",
23
24
  items: {
24
25
  type: "string"
25
26
  }
@@ -47,20 +48,8 @@ module SchwabMCP
47
48
  end
48
49
 
49
50
  begin
50
- client = SchwabRb::Auth.init_client_easy(
51
- ENV['SCHWAB_API_KEY'],
52
- ENV['SCHWAB_APP_SECRET'],
53
- ENV['SCHWAB_CALLBACK_URI'],
54
- ENV['TOKEN_PATH']
55
- )
56
-
57
- unless client
58
- log_error("Failed to initialize Schwab client")
59
- return MCP::Tool::Response.new([{
60
- type: "text",
61
- text: "**Error**: Failed to initialize Schwab client. Check your credentials."
62
- }])
63
- end
51
+ client = SchwabClientFactory.create_client
52
+ return SchwabClientFactory.client_error_response unless client
64
53
 
65
54
  account_id = ENV[account_name]
66
55
  unless account_id
@@ -75,9 +64,9 @@ module SchwabMCP
75
64
  log_debug("Found account ID: [REDACTED] for account name: #{account_name}")
76
65
  log_debug("Fetching account numbers mapping")
77
66
 
78
- account_numbers_response = client.get_account_numbers
67
+ account_numbers = client.get_account_numbers
79
68
 
80
- unless account_numbers_response&.body
69
+ unless account_numbers
81
70
  log_error("Failed to retrieve account numbers")
82
71
  return MCP::Tool::Response.new([{
83
72
  type: "text",
@@ -85,36 +74,27 @@ module SchwabMCP
85
74
  }])
86
75
  end
87
76
 
88
- account_mappings = JSON.parse(account_numbers_response.body, symbolize_names: true)
89
- log_debug("Account mappings retrieved (#{account_mappings.length} accounts found)")
77
+ log_debug("Account numbers retrieved (#{account_numbers.size} accounts found)")
90
78
 
91
- account_hash = nil
92
- account_mappings.each do |mapping|
93
- if mapping[:accountNumber] == account_id
94
- account_hash = mapping[:hashValue]
95
- break
96
- end
97
- end
79
+ account_hash = account_numbers.find_hash_value(account_id)
98
80
 
99
81
  unless account_hash
100
82
  log_error("Account ID not found in available accounts")
101
- available_accounts = account_mappings.map { |m| "[REDACTED]" }.join(", ")
102
83
  return MCP::Tool::Response.new([{
103
84
  type: "text",
104
- text: "**Error**: Account ID not found in available accounts. #{account_mappings.length} accounts available."
85
+ text: "**Error**: Account ID not found in available accounts. #{account_numbers.size} accounts available."
105
86
  }])
106
87
  end
107
88
 
108
89
  log_debug("Found account hash for account ID: #{account_name}")
109
-
110
90
  log_debug("Fetching account information with fields: #{fields}")
111
- account_response = client.get_account(account_hash, fields: fields)
112
91
 
113
- if account_response&.body
92
+ account = client.get_account(account_hash, fields: fields)
93
+
94
+ if account
114
95
  log_info("Successfully retrieved account information for #{account_name}")
115
- account_data = JSON.parse(account_response.body)
116
96
 
117
- formatted_response = format_account_data(account_data, account_name, account_id)
97
+ formatted_response = format_account_data(account, account_name, account_id)
118
98
 
119
99
  MCP::Tool::Response.new([{
120
100
  type: "text",
@@ -146,54 +126,58 @@ module SchwabMCP
146
126
 
147
127
  private
148
128
 
149
- def self.format_account_data(account_data, account_name, account_id)
150
- account = account_data["securitiesAccount"]
129
+ def self.format_account_data(account, account_name, account_id)
151
130
  friendly_name = account_name.gsub('_ACCOUNT', '').split('_').map(&:capitalize).join(' ')
152
131
 
153
132
  formatted = "**Account Information for #{friendly_name} (#{account_name}):**\n\n"
154
133
 
155
- if account
156
- formatted += "**Account Number:** #{Redactor::REDACTED_ACCOUNT_PLACEHOLDER}\n"
157
- formatted += "**Account Type:** #{account['type']}\n"
158
-
159
- if current_balances = account['currentBalances']
160
- formatted += "\n**Current Balances:**\n"
161
- formatted += "- Cash Balance: $#{format_currency(current_balances['cashBalance'])}\n"
162
- formatted += "- Buying Power: $#{format_currency(current_balances['buyingPower'])}\n"
163
- formatted += "- Total Cash: $#{format_currency(current_balances['totalCash'])}\n"
164
- formatted += "- Liquidation Value: $#{format_currency(current_balances['liquidationValue'])}\n"
165
- formatted += "- Long Market Value: $#{format_currency(current_balances['longMarketValue'])}\n"
166
- formatted += "- Short Market Value: $#{format_currency(current_balances['shortMarketValue'])}\n"
167
- end
134
+ formatted += "**Account Number:** #{Redactor::REDACTED_ACCOUNT_PLACEHOLDER}\n"
135
+ formatted += "**Account Type:** #{account.type}\n"
136
+
137
+ if current_balances = account.current_balances
138
+ formatted += "\n**Current Balances:**\n"
139
+ formatted += "- Cash Balance: $#{format_currency(current_balances.cash_balance)}\n"
140
+ formatted += "- Buying Power: $#{format_currency(current_balances.buying_power)}\n"
141
+ formatted += "- Liquidation Value: $#{format_currency(current_balances.liquidation_value)}\n"
142
+ formatted += "- Long Market Value: $#{format_currency(current_balances.long_market_value)}\n"
143
+ formatted += "- Short Market Value: $#{format_currency(current_balances.short_market_value)}\n"
144
+ end
168
145
 
169
- # Positions summary
170
- if positions = account['positions']
171
- formatted += "\n**Positions Summary:**\n"
172
- formatted += "- Total Positions: #{positions.length}\n"
173
-
174
- if positions.length > 0
175
- formatted += "\n**Position Details:**\n"
176
- positions.each do |position|
177
- symbol = position.dig('instrument', 'symbol')
178
- qty = position['longQuantity'].to_f - position['shortQuantity'].to_f
179
- market_value = position['marketValue']
180
- formatted += "- #{symbol}: #{qty} shares, Market Value: $#{format_currency(market_value)}\n"
181
- end
146
+ # Positions summary
147
+ if positions = account.positions
148
+ formatted += "\n**Positions Summary:**\n"
149
+ formatted += "- Total Positions: #{positions.length}\n"
150
+
151
+ if positions.length > 0
152
+ formatted += "\n**Position Details:**\n"
153
+ positions.each do |position|
154
+ symbol = position.instrument.symbol
155
+ qty = position.long_quantity - position.short_quantity
156
+ market_value = position.market_value
157
+ formatted += "- #{symbol}: #{qty} shares, Market Value: $#{format_currency(market_value)}\n"
182
158
  end
183
159
  end
160
+ end
184
161
 
185
- if orders = account['orderStrategies']
186
- formatted += "\n**Active Orders:**\n"
187
- formatted += "- Total Orders: #{orders.length}\n"
188
-
189
- orders.each do |order|
190
- status = order['status']
191
- symbol = order.dig('orderLegCollection', 0, 'instrument', 'symbol')
192
- formatted += "- #{symbol}: #{status}\n"
193
- end
194
- end end
162
+ # Note: Orders would need to be fetched separately as they're not part of the Account data object
163
+
164
+ # Convert account back to hash for JSON display (redacted)
165
+ account_hash = {
166
+ securitiesAccount: {
167
+ type: account.type,
168
+ accountNumber: account.account_number,
169
+ positions: account.positions.map(&:to_h),
170
+ currentBalances: {
171
+ cashBalance: account.current_balances&.cash_balance,
172
+ buyingPower: account.current_balances&.buying_power,
173
+ liquidationValue: account.current_balances&.liquidation_value,
174
+ longMarketValue: account.current_balances&.long_market_value,
175
+ shortMarketValue: account.current_balances&.short_market_value
176
+ }
177
+ }
178
+ }
195
179
 
196
- redacted_data = Redactor.redact(account_data)
180
+ redacted_data = Redactor.redact(account_hash)
197
181
  formatted += "\n```json\n#{JSON.pretty_generate(redacted_data)}\n```"
198
182
  formatted
199
183
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SchwabMCP
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/schwab_mcp.rb CHANGED
@@ -9,7 +9,6 @@ require_relative "schwab_mcp/redactor"
9
9
  require_relative "schwab_mcp/tools/quote_tool"
10
10
  require_relative "schwab_mcp/tools/quotes_tool"
11
11
  require_relative "schwab_mcp/tools/option_chain_tool"
12
- require_relative "schwab_mcp/tools/option_strategy_finder_tool"
13
12
  require_relative "schwab_mcp/tools/help_tool"
14
13
  require_relative "schwab_mcp/tools/schwab_account_details_tool"
15
14
  require_relative "schwab_mcp/tools/list_schwab_accounts_tool"
@@ -24,6 +23,7 @@ require_relative "schwab_mcp/tools/list_movers_tool"
24
23
  require_relative "schwab_mcp/tools/get_market_hours_tool"
25
24
  require_relative "schwab_mcp/tools/get_price_history_tool"
26
25
  require_relative "schwab_mcp/loggable"
26
+ require_relative "schwab_mcp/schwab_client_factory"
27
27
 
28
28
 
29
29
  module SchwabMCP
@@ -33,7 +33,6 @@ module SchwabMCP
33
33
  Tools::QuoteTool,
34
34
  Tools::QuotesTool,
35
35
  Tools::OptionChainTool,
36
- Tools::OptionStrategyFinderTool,
37
36
  Tools::HelpTool,
38
37
  Tools::SchwabAccountDetailsTool,
39
38
  Tools::ListSchwabAccountsTool,