schwab_mcp 0.1.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 +7 -0
- data/.copilotignore +3 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +157 -0
- data/Rakefile +12 -0
- data/exe/schwab_mcp +19 -0
- data/exe/schwab_token_refresh +38 -0
- data/exe/schwab_token_reset +49 -0
- data/lib/schwab_mcp/loggable.rb +31 -0
- data/lib/schwab_mcp/logger.rb +62 -0
- data/lib/schwab_mcp/option_chain_filter.rb +213 -0
- data/lib/schwab_mcp/orders/iron_condor_order.rb +87 -0
- data/lib/schwab_mcp/orders/order_factory.rb +40 -0
- data/lib/schwab_mcp/orders/vertical_order.rb +62 -0
- data/lib/schwab_mcp/redactor.rb +210 -0
- data/lib/schwab_mcp/resources/.keep +0 -0
- data/lib/schwab_mcp/tools/cancel_order_tool.rb +226 -0
- data/lib/schwab_mcp/tools/get_market_hours_tool.rb +104 -0
- data/lib/schwab_mcp/tools/get_order_tool.rb +263 -0
- data/lib/schwab_mcp/tools/get_price_history_tool.rb +203 -0
- data/lib/schwab_mcp/tools/help_tool.rb +406 -0
- data/lib/schwab_mcp/tools/list_account_orders_tool.rb +295 -0
- data/lib/schwab_mcp/tools/list_account_transactions_tool.rb +311 -0
- data/lib/schwab_mcp/tools/list_movers_tool.rb +125 -0
- data/lib/schwab_mcp/tools/list_schwab_accounts_tool.rb +162 -0
- data/lib/schwab_mcp/tools/option_chain_tool.rb +274 -0
- data/lib/schwab_mcp/tools/option_strategy_finder_tool.rb +378 -0
- data/lib/schwab_mcp/tools/place_order_tool.rb +305 -0
- data/lib/schwab_mcp/tools/preview_order_tool.rb +259 -0
- data/lib/schwab_mcp/tools/quote_tool.rb +77 -0
- data/lib/schwab_mcp/tools/quotes_tool.rb +110 -0
- data/lib/schwab_mcp/tools/replace_order_tool.rb +312 -0
- data/lib/schwab_mcp/tools/schwab_account_details_tool.rb +208 -0
- data/lib/schwab_mcp/version.rb +5 -0
- data/lib/schwab_mcp.rb +107 -0
- data/sig/schwab_mcp.rbs +4 -0
- data/start_mcp_server.sh +4 -0
- metadata +115 -0
@@ -0,0 +1,259 @@
|
|
1
|
+
require "mcp"
|
2
|
+
require "schwab_rb"
|
3
|
+
require "json"
|
4
|
+
require_relative "../loggable"
|
5
|
+
require_relative "../orders/order_factory"
|
6
|
+
require_relative "../redactor"
|
7
|
+
|
8
|
+
module SchwabMCP
|
9
|
+
module Tools
|
10
|
+
class PreviewOrderTool < MCP::Tool
|
11
|
+
extend Loggable
|
12
|
+
description "Preview an options order (iron condor, call spread, put spread) to validate the order structure and see estimated costs/proceeds before placing"
|
13
|
+
|
14
|
+
input_schema(
|
15
|
+
properties: {
|
16
|
+
account_name: {
|
17
|
+
type: "string",
|
18
|
+
description: "Account name mapped to environment variable ending with '_ACCOUNT' (e.g., 'TRADING_BROKERAGE_ACCOUNT')",
|
19
|
+
pattern: "^[A-Z_]+_ACCOUNT$"
|
20
|
+
},
|
21
|
+
strategy_type: {
|
22
|
+
type: "string",
|
23
|
+
enum: ["ironcondor", "callspread", "putspread"],
|
24
|
+
description: "Type of options strategy to preview"
|
25
|
+
},
|
26
|
+
price: {
|
27
|
+
type: "number",
|
28
|
+
description: "Net price for the order (credit for selling strategies, debit for buying strategies)"
|
29
|
+
},
|
30
|
+
quantity: {
|
31
|
+
type: "integer",
|
32
|
+
description: "Number of contracts (default: 1)",
|
33
|
+
default: 1
|
34
|
+
},
|
35
|
+
order_instruction: {
|
36
|
+
type: "string",
|
37
|
+
enum: ["open", "exit"],
|
38
|
+
description: "Whether to open a new position or exit an existing one (default: open)",
|
39
|
+
default: "open"
|
40
|
+
},
|
41
|
+
# Iron Condor specific fields
|
42
|
+
put_short_symbol: {
|
43
|
+
type: "string",
|
44
|
+
description: "Option symbol for the short put leg (required for iron condor)"
|
45
|
+
},
|
46
|
+
put_long_symbol: {
|
47
|
+
type: "string",
|
48
|
+
description: "Option symbol for the long put leg (required for iron condor)"
|
49
|
+
},
|
50
|
+
call_short_symbol: {
|
51
|
+
type: "string",
|
52
|
+
description: "Option symbol for the short call leg (required for iron condor)"
|
53
|
+
},
|
54
|
+
call_long_symbol: {
|
55
|
+
type: "string",
|
56
|
+
description: "Option symbol for the long call leg (required for iron condor)"
|
57
|
+
},
|
58
|
+
# Vertical spread specific fields
|
59
|
+
short_leg_symbol: {
|
60
|
+
type: "string",
|
61
|
+
description: "Option symbol for the short leg (required for call/put spreads)"
|
62
|
+
},
|
63
|
+
long_leg_symbol: {
|
64
|
+
type: "string",
|
65
|
+
description: "Option symbol for the long leg (required for call/put spreads)"
|
66
|
+
}
|
67
|
+
},
|
68
|
+
required: ["account_name", "strategy_type", "price"]
|
69
|
+
)
|
70
|
+
|
71
|
+
annotations(
|
72
|
+
title: "Preview Options Order",
|
73
|
+
read_only_hint: true,
|
74
|
+
destructive_hint: false,
|
75
|
+
idempotent_hint: true
|
76
|
+
)
|
77
|
+
|
78
|
+
def self.call(server_context:, **params)
|
79
|
+
log_info("Previewing #{params[:strategy_type]} order for account name: #{params[:account_name]}")
|
80
|
+
|
81
|
+
unless params[:account_name].end_with?('_ACCOUNT')
|
82
|
+
log_error("Invalid account name format: #{params[:account_name]}")
|
83
|
+
return MCP::Tool::Response.new([{
|
84
|
+
type: "text",
|
85
|
+
text: "**Error**: Account name must end with '_ACCOUNT'. Example: 'TRADING_BROKERAGE_ACCOUNT'"
|
86
|
+
}])
|
87
|
+
end
|
88
|
+
|
89
|
+
begin
|
90
|
+
validate_strategy_params(params)
|
91
|
+
client = SchwabRb::Auth.init_client_easy(
|
92
|
+
ENV['SCHWAB_API_KEY'],
|
93
|
+
ENV['SCHWAB_APP_SECRET'],
|
94
|
+
ENV['SCHWAB_CALLBACK_URI'],
|
95
|
+
ENV['TOKEN_PATH']
|
96
|
+
)
|
97
|
+
|
98
|
+
unless client
|
99
|
+
log_error("Failed to initialize Schwab client")
|
100
|
+
return MCP::Tool::Response.new([{
|
101
|
+
type: "text",
|
102
|
+
text: "**Error**: Failed to initialize Schwab client. Check your credentials."
|
103
|
+
}])
|
104
|
+
end
|
105
|
+
|
106
|
+
account_result = resolve_account_details(client, params[:account_name])
|
107
|
+
return account_result if account_result.is_a?(MCP::Tool::Response)
|
108
|
+
|
109
|
+
account_id, account_hash = account_result
|
110
|
+
|
111
|
+
order_builder = SchwabMCP::Orders::OrderFactory.build(
|
112
|
+
strategy_type: params[:strategy_type],
|
113
|
+
account_number: account_id,
|
114
|
+
price: params[:price],
|
115
|
+
quantity: params[:quantity] || 1,
|
116
|
+
order_instruction: (params[:order_instruction] || "open").to_sym,
|
117
|
+
# Iron Condor params
|
118
|
+
put_short_symbol: params[:put_short_symbol],
|
119
|
+
put_long_symbol: params[:put_long_symbol],
|
120
|
+
call_short_symbol: params[:call_short_symbol],
|
121
|
+
call_long_symbol: params[:call_long_symbol],
|
122
|
+
# Vertical spread params
|
123
|
+
short_leg_symbol: params[:short_leg_symbol],
|
124
|
+
long_leg_symbol: params[:long_leg_symbol]
|
125
|
+
)
|
126
|
+
|
127
|
+
log_debug("Making preview order API request")
|
128
|
+
response = client.preview_order(account_hash, order_builder)
|
129
|
+
|
130
|
+
if response&.body
|
131
|
+
log_info("Successfully previewed #{params[:strategy_type]} order")
|
132
|
+
formatted_response = format_preview_response(response.body, params)
|
133
|
+
MCP::Tool::Response.new([{
|
134
|
+
type: "text",
|
135
|
+
text: formatted_response
|
136
|
+
}])
|
137
|
+
else
|
138
|
+
log_warn("Empty response from Schwab API for order preview")
|
139
|
+
MCP::Tool::Response.new([{
|
140
|
+
type: "text",
|
141
|
+
text: "**No Data**: Empty response from Schwab API for order preview"
|
142
|
+
}])
|
143
|
+
end
|
144
|
+
|
145
|
+
rescue => e
|
146
|
+
log_error("Error previewing #{params[:strategy_type]} order: #{e.message}")
|
147
|
+
log_debug("Backtrace: #{e.backtrace.first(5).join('\n')}")
|
148
|
+
MCP::Tool::Response.new([{
|
149
|
+
type: "text",
|
150
|
+
text: "**Error** previewing #{params[:strategy_type]} order: #{e.message}\n\n#{e.backtrace.first(3).join('\n')}"
|
151
|
+
}])
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
def self.resolve_account_details(client, account_name)
|
158
|
+
account_id = ENV[account_name]
|
159
|
+
unless account_id
|
160
|
+
available_accounts = ENV.keys.select { |key| key.end_with?('_ACCOUNT') }
|
161
|
+
log_error("Account name '#{account_name}' not found in environment variables")
|
162
|
+
return MCP::Tool::Response.new([{
|
163
|
+
type: "text",
|
164
|
+
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."
|
165
|
+
}])
|
166
|
+
end
|
167
|
+
|
168
|
+
log_debug("Found account ID: [REDACTED] for account name: #{account_name}")
|
169
|
+
log_debug("Fetching account numbers mapping")
|
170
|
+
|
171
|
+
account_numbers_response = client.get_account_numbers
|
172
|
+
|
173
|
+
unless account_numbers_response&.body
|
174
|
+
log_error("Failed to retrieve account numbers")
|
175
|
+
return MCP::Tool::Response.new([{
|
176
|
+
type: "text",
|
177
|
+
text: "**Error**: Failed to retrieve account numbers from Schwab API"
|
178
|
+
}])
|
179
|
+
end
|
180
|
+
|
181
|
+
account_mappings = JSON.parse(account_numbers_response.body, symbolize_names: true)
|
182
|
+
log_debug("Account mappings retrieved (#{account_mappings.length} accounts found)")
|
183
|
+
|
184
|
+
account_hash = nil
|
185
|
+
account_mappings.each do |mapping|
|
186
|
+
if mapping[:accountNumber] == account_id
|
187
|
+
account_hash = mapping[:hashValue]
|
188
|
+
break
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
unless account_hash
|
193
|
+
log_error("Account ID not found in available accounts")
|
194
|
+
return MCP::Tool::Response.new([{
|
195
|
+
type: "text",
|
196
|
+
text: "**Error**: Account ID not found in available accounts. #{account_mappings.length} accounts available."
|
197
|
+
}])
|
198
|
+
end
|
199
|
+
|
200
|
+
log_debug("Found account hash for account name: #{account_name}")
|
201
|
+
[account_id, account_hash]
|
202
|
+
end
|
203
|
+
|
204
|
+
def self.validate_strategy_params(params)
|
205
|
+
case params[:strategy_type]
|
206
|
+
when 'ironcondor'
|
207
|
+
required_fields = [:put_short_symbol, :put_long_symbol, :call_short_symbol, :call_long_symbol]
|
208
|
+
missing_fields = required_fields.select { |field| params[field].nil? || params[field].empty? }
|
209
|
+
unless missing_fields.empty?
|
210
|
+
raise ArgumentError, "Iron condor strategy requires: #{missing_fields.join(', ')}"
|
211
|
+
end
|
212
|
+
when 'callspread', 'putspread'
|
213
|
+
required_fields = [:short_leg_symbol, :long_leg_symbol]
|
214
|
+
missing_fields = required_fields.select { |field| params[field].nil? || params[field].empty? }
|
215
|
+
unless missing_fields.empty?
|
216
|
+
raise ArgumentError, "#{params[:strategy_type]} strategy requires: #{missing_fields.join(', ')}"
|
217
|
+
end
|
218
|
+
else
|
219
|
+
raise ArgumentError, "Unsupported strategy type: #{params[:strategy_type]}"
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def self.format_preview_response(response_body, params)
|
224
|
+
parsed = JSON.parse(response_body)
|
225
|
+
redacted_data = Redactor.redact(parsed)
|
226
|
+
|
227
|
+
begin
|
228
|
+
strategy_summary = case params[:strategy_type]
|
229
|
+
when 'ironcondor'
|
230
|
+
"**Iron Condor Preview**\n" \
|
231
|
+
"- Put Short: #{params[:put_short_symbol]}\n" \
|
232
|
+
"- Put Long: #{params[:put_long_symbol]}\n" \
|
233
|
+
"- Call Short: #{params[:call_short_symbol]}\n" \
|
234
|
+
"- Call Long: #{params[:call_long_symbol]}\n"
|
235
|
+
when 'callspread', 'putspread'
|
236
|
+
"**#{params[:strategy_type].capitalize} Preview**\n" \
|
237
|
+
"- Short Leg: #{params[:short_leg_symbol]}\n" \
|
238
|
+
"- Long Leg: #{params[:long_leg_symbol]}\n"
|
239
|
+
end
|
240
|
+
|
241
|
+
friendly_name = params[:account_name].gsub('_ACCOUNT', '').split('_').map(&:capitalize).join(' ')
|
242
|
+
|
243
|
+
order_details = "**Order Details:**\n" \
|
244
|
+
"- Strategy: #{params[:strategy_type]}\n" \
|
245
|
+
"- Action: #{params[:order_instruction] || 'open'}\n" \
|
246
|
+
"- Quantity: #{params[:quantity] || 1}\n" \
|
247
|
+
"- Price: $#{params[:price]}\n" \
|
248
|
+
"- Account: #{friendly_name} (#{params[:account_name]})\n\n"
|
249
|
+
|
250
|
+
full_response = "**Schwab API Preview Response:**\n\n```json\n#{JSON.pretty_generate(redacted_data)}\n```"
|
251
|
+
|
252
|
+
"#{strategy_summary}\n#{order_details}#{full_response}"
|
253
|
+
rescue JSON::ParserError
|
254
|
+
"**Order Preview Response:**\n\n```\n#{JSON.pretty_generate(redacted_data)}\n```"
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|
259
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require "mcp"
|
2
|
+
require "schwab_rb"
|
3
|
+
require "json"
|
4
|
+
require_relative "../loggable"
|
5
|
+
|
6
|
+
module SchwabMCP
|
7
|
+
module Tools
|
8
|
+
class QuoteTool < MCP::Tool
|
9
|
+
extend Loggable
|
10
|
+
description "Get a real-time quote for a single instrument symbol using Schwab API"
|
11
|
+
|
12
|
+
input_schema(
|
13
|
+
properties: {
|
14
|
+
symbol: {
|
15
|
+
type: "string",
|
16
|
+
description: "Instrument symbol (e.g., 'AAPL', 'TSLA')",
|
17
|
+
pattern: "^[A-Za-z]{1,5}$"
|
18
|
+
}
|
19
|
+
},
|
20
|
+
required: ["symbol"]
|
21
|
+
)
|
22
|
+
|
23
|
+
annotations(
|
24
|
+
title: "Get Financial Instrument Quote",
|
25
|
+
read_only_hint: true,
|
26
|
+
destructive_hint: false,
|
27
|
+
idempotent_hint: true
|
28
|
+
)
|
29
|
+
|
30
|
+
def self.call(symbol:, server_context:)
|
31
|
+
log_info("Getting quote for symbol: #{symbol}")
|
32
|
+
|
33
|
+
begin
|
34
|
+
client = SchwabRb::Auth.init_client_easy(
|
35
|
+
ENV['SCHWAB_API_KEY'],
|
36
|
+
ENV['SCHWAB_APP_SECRET'],
|
37
|
+
ENV['SCHWAB_CALLBACK_URI'],
|
38
|
+
ENV['TOKEN_PATH']
|
39
|
+
)
|
40
|
+
|
41
|
+
unless client
|
42
|
+
log_error("Failed to initialize Schwab client")
|
43
|
+
return MCP::Tool::Response.new([{
|
44
|
+
type: "text",
|
45
|
+
text: "**Error**: Failed to initialize Schwab client. Check your credentials."
|
46
|
+
}])
|
47
|
+
end
|
48
|
+
|
49
|
+
log_debug("Making API request for symbol: #{symbol}")
|
50
|
+
response = client.get_quote(symbol.upcase)
|
51
|
+
|
52
|
+
if response&.body
|
53
|
+
log_info("Successfully retrieved quote for #{symbol}")
|
54
|
+
MCP::Tool::Response.new([{
|
55
|
+
type: "text",
|
56
|
+
text: "**Quote for #{symbol.upcase}:**\n\n```json\n#{response.body}\n```"
|
57
|
+
}])
|
58
|
+
else
|
59
|
+
log_warn("Empty response from Schwab API for symbol: #{symbol}")
|
60
|
+
MCP::Tool::Response.new([{
|
61
|
+
type: "text",
|
62
|
+
text: "**No Data**: Empty response from Schwab API for symbol: #{symbol}"
|
63
|
+
}])
|
64
|
+
end
|
65
|
+
|
66
|
+
rescue => e
|
67
|
+
log_error("Error retrieving quote for #{symbol}: #{e.message}")
|
68
|
+
log_debug("Backtrace: #{e.backtrace.first(3).join('\n')}")
|
69
|
+
MCP::Tool::Response.new([{
|
70
|
+
type: "text",
|
71
|
+
text: "**Error** retrieving quote for #{symbol}: #{e.message}\n\n#{e.backtrace.first(3).join('\n')}"
|
72
|
+
}])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require "mcp"
|
2
|
+
require "schwab_rb"
|
3
|
+
require "json"
|
4
|
+
require_relative "../loggable"
|
5
|
+
|
6
|
+
module SchwabMCP
|
7
|
+
module Tools
|
8
|
+
class QuotesTool < MCP::Tool
|
9
|
+
extend Loggable
|
10
|
+
|
11
|
+
description "Get real-time quotes for multiple instrument symbols using Schwab API formatted as JSON"
|
12
|
+
|
13
|
+
input_schema(
|
14
|
+
properties: {
|
15
|
+
symbols: {
|
16
|
+
type: "array",
|
17
|
+
items: {
|
18
|
+
type: "string",
|
19
|
+
pattern: "^[A-Za-z0-9/.$-]{1,12}$"
|
20
|
+
},
|
21
|
+
description: "Array of instrument symbols (e.g., ['AAPL', 'TSLA', '/ES']) - supports futures and other special symbols",
|
22
|
+
minItems: 1,
|
23
|
+
maxItems: 500
|
24
|
+
},
|
25
|
+
fields: {
|
26
|
+
type: "array",
|
27
|
+
items: {
|
28
|
+
type: "string"
|
29
|
+
},
|
30
|
+
description: "Optional array of specific quote fields to return. If not specified, returns all available data."
|
31
|
+
},
|
32
|
+
indicative: {
|
33
|
+
type: "boolean",
|
34
|
+
description: "Optional flag to fetch indicative quotes (true/false). If not specified, returns standard quotes."
|
35
|
+
}
|
36
|
+
},
|
37
|
+
required: ["symbols"]
|
38
|
+
)
|
39
|
+
|
40
|
+
annotations(
|
41
|
+
title: "Get Financial Instrument Quotes",
|
42
|
+
read_only_hint: true,
|
43
|
+
destructive_hint: false,
|
44
|
+
idempotent_hint: true
|
45
|
+
)
|
46
|
+
|
47
|
+
def self.call(symbols:, fields: ["quote"], indicative: false, server_context:)
|
48
|
+
symbols = [symbols] if symbols.is_a?(String)
|
49
|
+
|
50
|
+
log_info("Getting quotes for #{symbols.length} symbols: #{symbols.join(', ')}")
|
51
|
+
|
52
|
+
begin
|
53
|
+
client = SchwabRb::Auth.init_client_easy(
|
54
|
+
ENV['SCHWAB_API_KEY'],
|
55
|
+
ENV['SCHWAB_APP_SECRET'],
|
56
|
+
ENV['SCHWAB_CALLBACK_URI'],
|
57
|
+
ENV['TOKEN_PATH']
|
58
|
+
)
|
59
|
+
|
60
|
+
unless client
|
61
|
+
log_error("Failed to initialize Schwab client")
|
62
|
+
return MCP::Tool::Response.new([{
|
63
|
+
type: "text",
|
64
|
+
text: "**Error**: Failed to initialize Schwab client. Check your credentials."
|
65
|
+
}])
|
66
|
+
end
|
67
|
+
|
68
|
+
log_debug("Making API request for symbols: #{symbols.join(', ')}")
|
69
|
+
log_debug("Fields: #{fields || 'all'}")
|
70
|
+
log_debug("Indicative: #{indicative || 'not specified'}")
|
71
|
+
|
72
|
+
normalized_symbols = symbols.map(&:upcase)
|
73
|
+
|
74
|
+
response = client.get_quotes(
|
75
|
+
normalized_symbols,
|
76
|
+
fields: fields,
|
77
|
+
indicative: indicative
|
78
|
+
)
|
79
|
+
|
80
|
+
if response&.body
|
81
|
+
log_info("Successfully retrieved quotes for #{symbols.length} symbols")
|
82
|
+
|
83
|
+
symbol_list = normalized_symbols.join(', ')
|
84
|
+
field_info = fields ? " (fields: #{fields.join(', ')})" : " (all fields)"
|
85
|
+
indicative_info = indicative.nil? ? "" : " (indicative: #{indicative})"
|
86
|
+
|
87
|
+
MCP::Tool::Response.new([{
|
88
|
+
type: "text",
|
89
|
+
text: "**Quotes for #{symbol_list}:**#{field_info}#{indicative_info}\n\n```json\n#{response.body}\n```"
|
90
|
+
}])
|
91
|
+
else
|
92
|
+
log_warn("Empty response from Schwab API for symbols: #{symbols.join(', ')}")
|
93
|
+
MCP::Tool::Response.new([{
|
94
|
+
type: "text",
|
95
|
+
text: "**No Data**: Empty response from Schwab API for symbols: #{symbols.join(', ')}"
|
96
|
+
}])
|
97
|
+
end
|
98
|
+
|
99
|
+
rescue => e
|
100
|
+
log_error("Error retrieving quotes for #{symbols.join(', ')}: #{e.message}")
|
101
|
+
log_debug("Backtrace: #{e.backtrace.first(3).join('\n')}")
|
102
|
+
MCP::Tool::Response.new([{
|
103
|
+
type: "text",
|
104
|
+
text: "**Error** retrieving quotes for #{symbols.join(', ')}: #{e.message}\n\n#{e.backtrace.first(3).join('\n')}"
|
105
|
+
}])
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|