groww-mcp 1.0.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.
@@ -0,0 +1,207 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mcp"
4
+
5
+ module GrowwMcp
6
+ module Tools
7
+ class GetQuote < GrowwMcp::BaseTool
8
+ description "Get live market quote for a stock or F&O instrument. " \
9
+ "Returns last price, OHLC, bid/ask, volume, and change %. " \
10
+ "⚠️ Requires Groww Live Data subscription (₹499/mo)."
11
+
12
+ input_schema(
13
+ properties: {
14
+ trading_symbol: {
15
+ type: "string",
16
+ description: "Trading symbol (e.g., RELIANCE, NIFTY24JUL25000CE)",
17
+ },
18
+ exchange: {
19
+ type: "string",
20
+ enum: %w[NSE BSE],
21
+ description: "Exchange (default: NSE)",
22
+ },
23
+ segment: {
24
+ type: "string",
25
+ enum: %w[CASH FNO],
26
+ description: "Market segment (default: CASH)",
27
+ },
28
+ },
29
+ required: ["trading_symbol"],
30
+ )
31
+
32
+ class << self
33
+ def call(server_context:, trading_symbol:, exchange: "NSE", segment: "CASH")
34
+ client = server_context[:client]
35
+ result = client.quote(trading_symbol, exchange: exchange, segment: segment)
36
+ format_response(result)
37
+ rescue GrowwMcp::ForbiddenError
38
+ MCP::Tool::Response.new([{
39
+ type: "text",
40
+ text: "❌ Live Data access denied. This requires the Groww Live Data subscription (₹499 + GST/month). " \
41
+ "Subscribe at: Groww App → Settings → Trading APIs.",
42
+ }])
43
+ rescue GrowwMcp::ApiError => e
44
+ error_response(e)
45
+ end
46
+ end
47
+ end
48
+
49
+ class GetLTP < GrowwMcp::BaseTool
50
+ description "Get last traded price (LTP) for multiple stocks at once (max 50 symbols). " \
51
+ "Symbols are sent to the API as comma-separated EXCHANGE_SYMBOL pairs " \
52
+ "(e.g. NSE_RELIANCE,NSE_TCS) — bare symbols are automatically prefixed with the exchange. " \
53
+ "⚠️ Requires Groww Live Data subscription (₹499/mo)."
54
+
55
+ input_schema(
56
+ properties: {
57
+ symbols: {
58
+ type: "array",
59
+ items: { type: "string" },
60
+ description: "Array of trading symbols (max 50). Bare symbols (e.g. RELIANCE) get the " \
61
+ "exchange prefix added automatically; already-prefixed pairs (e.g. NSE_RELIANCE) " \
62
+ "are used as-is.",
63
+ },
64
+ exchange: {
65
+ type: "string",
66
+ enum: %w[NSE BSE],
67
+ description: "Exchange used to prefix bare symbols (default: NSE)",
68
+ },
69
+ segment: {
70
+ type: "string",
71
+ enum: %w[CASH FNO],
72
+ description: "Market segment (default: CASH)",
73
+ },
74
+ },
75
+ required: ["symbols"],
76
+ )
77
+
78
+ class << self
79
+ def call(server_context:, symbols:, exchange: "NSE", segment: "CASH")
80
+ if symbols.length > 50
81
+ return MCP::Tool::Response.new([{ type: "text", text: "❌ Maximum 50 symbols allowed per request." }])
82
+ end
83
+
84
+ client = server_context[:client]
85
+ result = client.ltp(symbols, exchange: exchange, segment: segment)
86
+ format_response(result)
87
+ rescue GrowwMcp::ForbiddenError
88
+ MCP::Tool::Response.new([{
89
+ type: "text",
90
+ text: "❌ Live Data access denied. Requires Groww Live Data subscription (₹499 + GST/month).",
91
+ }])
92
+ rescue GrowwMcp::ApiError => e
93
+ error_response(e)
94
+ end
95
+ end
96
+ end
97
+
98
+ class GetOHLC < GrowwMcp::BaseTool
99
+ description "Get Open, High, Low, Close prices for multiple stocks (max 50). " \
100
+ "Symbols are sent to the API as comma-separated EXCHANGE_SYMBOL pairs " \
101
+ "(e.g. NSE_RELIANCE,NSE_TCS) — bare symbols are automatically prefixed with the exchange. " \
102
+ "⚠️ Requires Groww Live Data subscription (₹499/mo)."
103
+
104
+ input_schema(
105
+ properties: {
106
+ symbols: {
107
+ type: "array",
108
+ items: { type: "string" },
109
+ description: "Array of trading symbols (max 50). Bare symbols (e.g. RELIANCE) get the " \
110
+ "exchange prefix added automatically; already-prefixed pairs (e.g. NSE_RELIANCE) " \
111
+ "are used as-is.",
112
+ },
113
+ exchange: {
114
+ type: "string",
115
+ enum: %w[NSE BSE],
116
+ description: "Exchange used to prefix bare symbols (default: NSE)",
117
+ },
118
+ segment: {
119
+ type: "string",
120
+ enum: %w[CASH FNO],
121
+ description: "Market segment (default: CASH)",
122
+ },
123
+ },
124
+ required: ["symbols"],
125
+ )
126
+
127
+ class << self
128
+ def call(server_context:, symbols:, exchange: "NSE", segment: "CASH")
129
+ if symbols.length > 50
130
+ return MCP::Tool::Response.new([{ type: "text", text: "❌ Maximum 50 symbols allowed per request." }])
131
+ end
132
+
133
+ client = server_context[:client]
134
+ result = client.ohlc(symbols, exchange: exchange, segment: segment)
135
+ format_response(result)
136
+ rescue GrowwMcp::ForbiddenError
137
+ MCP::Tool::Response.new([{
138
+ type: "text",
139
+ text: "❌ Live Data access denied. Requires Groww Live Data subscription (₹499 + GST/month).",
140
+ }])
141
+ rescue GrowwMcp::ApiError => e
142
+ error_response(e)
143
+ end
144
+ end
145
+ end
146
+
147
+ class GetHistoricalData < GrowwMcp::BaseTool
148
+ description "Get historical candlestick (OHLCV) data for a stock or F&O instrument. " \
149
+ "Useful for technical analysis and backtesting. " \
150
+ "⚠️ Requires Groww Live Data subscription (₹499/mo)."
151
+
152
+ input_schema(
153
+ properties: {
154
+ trading_symbol: {
155
+ type: "string",
156
+ description: "Trading symbol (e.g., RELIANCE, NIFTY24JUL25000CE)",
157
+ },
158
+ start_time: {
159
+ type: %w[integer string],
160
+ description: "Start time — either a \"yyyy-MM-dd HH:mm:ss\" string (e.g. \"2026-07-01 09:15:00\") " \
161
+ "or a numeric epoch timestamp (epoch seconds are auto-converted to the " \
162
+ "milliseconds the API expects; epoch milliseconds pass through as-is)",
163
+ },
164
+ end_time: {
165
+ type: %w[integer string],
166
+ description: "End time — either a \"yyyy-MM-dd HH:mm:ss\" string (e.g. \"2026-07-22 15:30:00\") " \
167
+ "or a numeric epoch timestamp (epoch seconds are auto-converted to the " \
168
+ "milliseconds the API expects; epoch milliseconds pass through as-is)",
169
+ },
170
+ exchange: {
171
+ type: "string",
172
+ enum: %w[NSE BSE],
173
+ description: "Exchange (default: NSE)",
174
+ },
175
+ segment: {
176
+ type: "string",
177
+ enum: %w[CASH FNO],
178
+ description: "Market segment (default: CASH)",
179
+ },
180
+ interval_in_minutes: {
181
+ type: "integer",
182
+ description: "Candle interval in minutes (optional — e.g. 1, 5, 15, 60, 1440 for daily)",
183
+ },
184
+ },
185
+ required: %w[trading_symbol start_time end_time],
186
+ )
187
+
188
+ class << self
189
+ def call(server_context:, trading_symbol:, start_time:, end_time:, exchange: "NSE", segment: "CASH",
190
+ interval_in_minutes: nil)
191
+ client = server_context[:client]
192
+ result = client.historical_candles(trading_symbol, start_time, end_time,
193
+ exchange: exchange, segment: segment,
194
+ interval_in_minutes: interval_in_minutes)
195
+ format_response(result)
196
+ rescue GrowwMcp::ForbiddenError
197
+ MCP::Tool::Response.new([{
198
+ type: "text",
199
+ text: "❌ Historical Data access denied. Requires Groww Live Data subscription (₹499 + GST/month).",
200
+ }])
201
+ rescue GrowwMcp::ApiError => e
202
+ error_response(e)
203
+ end
204
+ end
205
+ end
206
+ end
207
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrowwMcp
4
+ module Tools
5
+ class GetOptionChain < GrowwMcp::BaseTool
6
+ description "Get the complete option chain for an underlying instrument (e.g., NIFTY, BANKNIFTY, RELIANCE) " \
7
+ "for a specific expiry date. " \
8
+ "Returns all available strike prices with CE and PE data including LTP, OI (Open Interest), " \
9
+ "volume, IV (Implied Volatility), and Greeks (Delta, Gamma, Theta, Vega). " \
10
+ "Essential for options analysis and strategy selection. " \
11
+ "⚠️ Requires Groww Live Data subscription (₹499/mo)."
12
+
13
+ input_schema(
14
+ properties: {
15
+ trading_symbol: {
16
+ type: "string",
17
+ description: "Underlying symbol (e.g., NIFTY, BANKNIFTY, RELIANCE)",
18
+ },
19
+ expiry_date: {
20
+ type: "string",
21
+ description: "Expiry date in yyyy-MM-dd format (e.g., 2026-07-30)",
22
+ },
23
+ exchange: {
24
+ type: "string",
25
+ enum: %w[NSE BSE],
26
+ description: "Exchange (default: NSE)",
27
+ },
28
+ },
29
+ required: %w[trading_symbol expiry_date],
30
+ )
31
+
32
+ class << self
33
+ def call(server_context:, trading_symbol:, expiry_date:, exchange: "NSE")
34
+ client = server_context[:client]
35
+ result = client.option_chain(trading_symbol, expiry_date: expiry_date, exchange: exchange)
36
+ format_response(result)
37
+ rescue GrowwMcp::ForbiddenError
38
+ MCP::Tool::Response.new([{
39
+ type: "text",
40
+ text: "❌ Option Chain access denied. Requires Groww Live Data subscription (₹499 + GST/month). " \
41
+ "Subscribe at: Groww App → Settings → Trading APIs.",
42
+ }])
43
+ rescue GrowwMcp::ApiError => e
44
+ error_response(e)
45
+ end
46
+ end
47
+ end
48
+
49
+ class GetGreeks < GrowwMcp::BaseTool
50
+ description "Get option Greeks (Delta, Gamma, Theta, Vega, Rho) and IV for a specific option contract. " \
51
+ "Provide the option's trading symbol, its underlying, and the expiry date. " \
52
+ "⚠️ Requires Groww Live Data subscription (₹499/mo)."
53
+
54
+ input_schema(
55
+ properties: {
56
+ trading_symbol: {
57
+ type: "string",
58
+ description: "Option contract trading symbol (e.g., NIFTY24JUL25000CE)",
59
+ },
60
+ underlying: {
61
+ type: "string",
62
+ description: "Underlying symbol (e.g., NIFTY, BANKNIFTY, RELIANCE)",
63
+ },
64
+ expiry: {
65
+ type: "string",
66
+ description: "Expiry date in yyyy-MM-dd format (e.g., 2026-07-30)",
67
+ },
68
+ exchange: {
69
+ type: "string",
70
+ enum: %w[NSE BSE],
71
+ description: "Exchange (default: NSE)",
72
+ },
73
+ },
74
+ required: %w[trading_symbol underlying expiry],
75
+ )
76
+
77
+ class << self
78
+ def call(server_context:, trading_symbol:, underlying:, expiry:, exchange: "NSE")
79
+ client = server_context[:client]
80
+ result = client.greeks(trading_symbol: trading_symbol, underlying: underlying,
81
+ expiry: expiry, exchange: exchange)
82
+ format_response(result)
83
+ rescue GrowwMcp::ForbiddenError
84
+ MCP::Tool::Response.new([{
85
+ type: "text",
86
+ text: "❌ Greeks access denied. Requires Groww Live Data subscription (₹499 + GST/month). " \
87
+ "Subscribe at: Groww App → Settings → Trading APIs.",
88
+ }])
89
+ rescue GrowwMcp::ApiError => e
90
+ error_response(e)
91
+ end
92
+ end
93
+ end
94
+
95
+ class GetOrderTrades < GrowwMcp::BaseTool
96
+ description "Get all trade executions for a specific order. " \
97
+ "An order can have multiple partial fills — this shows each fill with price, quantity, and timestamp."
98
+
99
+ input_schema(
100
+ properties: {
101
+ order_id: {
102
+ type: "string",
103
+ description: "Groww order ID",
104
+ },
105
+ segment: {
106
+ type: "string",
107
+ enum: %w[CASH FNO],
108
+ description: "Market segment (default: CASH)",
109
+ },
110
+ },
111
+ required: ["order_id"],
112
+ )
113
+
114
+ class << self
115
+ def call(server_context:, order_id:, segment: "CASH")
116
+ client = server_context[:client]
117
+ result = client.order_trades(order_id, segment: segment)
118
+ format_response(result)
119
+ rescue GrowwMcp::ApiError => e
120
+ error_response(e)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mcp"
4
+
5
+ module GrowwMcp
6
+ module Tools
7
+ class GetOrders < GrowwMcp::BaseTool
8
+ description "Get list of all orders. Optionally filter by market segment (CASH/FNO) " \
9
+ "and paginate results. Shows order status, prices, quantities, and timestamps."
10
+
11
+ input_schema(
12
+ properties: {
13
+ segment: {
14
+ type: "string",
15
+ enum: %w[CASH FNO],
16
+ description: "Filter by market segment (optional)",
17
+ },
18
+ page: {
19
+ type: "integer",
20
+ description: "Page number for pagination (optional)",
21
+ },
22
+ page_size: {
23
+ type: "integer",
24
+ description: "Number of orders per page (optional, default: 20)",
25
+ },
26
+ },
27
+ )
28
+
29
+ class << self
30
+ def call(server_context:, segment: nil, page: nil, page_size: nil)
31
+ client = server_context[:client]
32
+ result = client.order_list(segment: segment, page: page, page_size: page_size)
33
+ format_response(result)
34
+ rescue GrowwMcp::ApiError => e
35
+ error_response(e)
36
+ end
37
+ end
38
+ end
39
+
40
+ class GetOrderDetail < GrowwMcp::BaseTool
41
+ description "Get detailed status of a specific order by its Groww order ID. " \
42
+ "Shows fill price, pending quantity, rejection reason, and timestamps."
43
+
44
+ input_schema(
45
+ properties: {
46
+ order_id: {
47
+ type: "string",
48
+ description: "Groww order ID",
49
+ },
50
+ segment: {
51
+ type: "string",
52
+ enum: %w[CASH FNO],
53
+ description: "Market segment (default: CASH)",
54
+ },
55
+ },
56
+ required: ["order_id"],
57
+ )
58
+
59
+ class << self
60
+ def call(server_context:, order_id:, segment: "CASH")
61
+ client = server_context[:client]
62
+ result = client.order_detail(order_id, segment: segment)
63
+ format_response(result)
64
+ rescue GrowwMcp::ApiError => e
65
+ error_response(e)
66
+ end
67
+ end
68
+ end
69
+
70
+ class PlaceOrder < GrowwMcp::BaseTool
71
+ description "Place a buy or sell order on Groww. Supports MARKET, LIMIT, SL, and SL_M order types. " \
72
+ "For stocks use segment=CASH with product CNC (delivery) or MIS (intraday). " \
73
+ "For F&O use segment=FNO with product NRML. " \
74
+ "⚠️ This executes a REAL trade with REAL money. Always confirm with the user first."
75
+
76
+ input_schema(
77
+ properties: {
78
+ trading_symbol: {
79
+ type: "string",
80
+ description: "Stock/option symbol (e.g., RELIANCE, NIFTY24JUL25000CE)",
81
+ },
82
+ exchange: {
83
+ type: "string",
84
+ enum: %w[NSE BSE],
85
+ description: "Exchange (default: NSE)",
86
+ },
87
+ transaction_type: {
88
+ type: "string",
89
+ enum: %w[BUY SELL],
90
+ description: "Buy or sell",
91
+ },
92
+ order_type: {
93
+ type: "string",
94
+ enum: %w[MARKET LIMIT SL SL_M],
95
+ description: "MARKET=instant, LIMIT=at price, SL=stop loss limit, SL_M=stop loss market",
96
+ },
97
+ quantity: {
98
+ type: "integer",
99
+ description: "Number of shares/lots",
100
+ },
101
+ price: {
102
+ type: "number",
103
+ description: "Limit price (required for LIMIT and SL orders)",
104
+ },
105
+ trigger_price: {
106
+ type: "number",
107
+ description: "Trigger price (required for SL and SL_M orders)",
108
+ },
109
+ product: {
110
+ type: "string",
111
+ enum: %w[CNC MIS NRML],
112
+ description: "CNC=delivery, MIS=intraday, NRML=F&O",
113
+ },
114
+ segment: {
115
+ type: "string",
116
+ enum: %w[CASH FNO],
117
+ description: "Market segment (default: CASH)",
118
+ },
119
+ validity: {
120
+ type: "string",
121
+ enum: %w[DAY],
122
+ description: "Order validity (default: DAY)",
123
+ },
124
+ },
125
+ required: %w[trading_symbol transaction_type order_type quantity product],
126
+ )
127
+
128
+ class << self
129
+ def call(server_context:, trading_symbol:, transaction_type:, order_type:, quantity:, product:,
130
+ exchange: "NSE", price: nil, trigger_price: nil, segment: "CASH", validity: "DAY")
131
+ client = server_context[:client]
132
+ order = {
133
+ trading_symbol: trading_symbol,
134
+ exchange: exchange,
135
+ transaction_type: transaction_type,
136
+ order_type: order_type,
137
+ quantity: quantity,
138
+ product: product,
139
+ segment: segment,
140
+ validity: validity,
141
+ }
142
+ order[:price] = price if price
143
+ order[:trigger_price] = trigger_price if trigger_price
144
+
145
+ result = client.place_order(order)
146
+ format_response(result)
147
+ rescue GrowwMcp::ApiError => e
148
+ error_response(e)
149
+ end
150
+ end
151
+ end
152
+
153
+ class ModifyOrder < GrowwMcp::BaseTool
154
+ description "Modify an existing open order (change quantity, price, or trigger price). " \
155
+ "The Groww API requires quantity, order_type, and segment on every modify — " \
156
+ "pass the order's current values for anything you are not changing."
157
+
158
+ input_schema(
159
+ properties: {
160
+ order_id: { type: "string", description: "Groww order ID to modify" },
161
+ quantity: { type: "integer", description: "Quantity (required — pass current value if unchanged)" },
162
+ order_type: { type: "string", enum: %w[MARKET LIMIT SL SL_M], description: "Order type (required — pass current value if unchanged)" },
163
+ segment: { type: "string", enum: %w[CASH FNO], description: "Market segment of the order" },
164
+ price: { type: "number", description: "New price (optional)" },
165
+ trigger_price: { type: "number", description: "New trigger price (optional)" },
166
+ },
167
+ required: %w[order_id quantity order_type segment],
168
+ )
169
+
170
+ class << self
171
+ def call(server_context:, order_id:, quantity:, order_type:, segment:, price: nil, trigger_price: nil)
172
+ client = server_context[:client]
173
+ mods = {
174
+ quantity: quantity,
175
+ order_type: order_type,
176
+ segment: segment,
177
+ }
178
+ mods[:price] = price if price
179
+ mods[:trigger_price] = trigger_price if trigger_price
180
+
181
+ result = client.modify_order(order_id, mods)
182
+ format_response(result)
183
+ rescue GrowwMcp::ApiError => e
184
+ error_response(e)
185
+ end
186
+ end
187
+ end
188
+
189
+ class CancelOrder < GrowwMcp::BaseTool
190
+ description "Cancel an open/pending order by its Groww order ID."
191
+
192
+ input_schema(
193
+ properties: {
194
+ order_id: { type: "string", description: "Groww order ID to cancel" },
195
+ segment: { type: "string", enum: %w[CASH FNO], description: "Market segment (default: CASH)" },
196
+ },
197
+ required: ["order_id"],
198
+ )
199
+
200
+ class << self
201
+ def call(server_context:, order_id:, segment: "CASH")
202
+ client = server_context[:client]
203
+ result = client.cancel_order(order_id, segment: segment)
204
+ format_response(result)
205
+ rescue GrowwMcp::ApiError => e
206
+ error_response(e)
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mcp"
4
+
5
+ module GrowwMcp
6
+ module Tools
7
+ class GetHoldings < GrowwMcp::BaseTool
8
+ description "Get all current stock holdings from your DEMAT account. " \
9
+ "Returns each stock with ISIN, trading symbol, quantity, average buy price, " \
10
+ "pledged/locked quantities, and tradable exchanges."
11
+
12
+ input_schema(properties: {})
13
+
14
+ class << self
15
+ def call(server_context:)
16
+ client = server_context[:client]
17
+ result = client.holdings
18
+ format_response(result)
19
+ rescue GrowwMcp::ApiError => e
20
+ error_response(e)
21
+ end
22
+ end
23
+ end
24
+
25
+ class GetPositions < GrowwMcp::BaseTool
26
+ description "Get all open intraday and F&O positions with unrealized P&L. " \
27
+ "Optionally filter by market segment (CASH or FNO)."
28
+
29
+ input_schema(
30
+ properties: {
31
+ segment: {
32
+ type: "string",
33
+ enum: %w[CASH FNO],
34
+ description: "Filter by market segment (optional)",
35
+ },
36
+ },
37
+ )
38
+
39
+ class << self
40
+ def call(server_context:, segment: nil)
41
+ client = server_context[:client]
42
+ result = client.positions(segment: segment)
43
+ format_response(result)
44
+ rescue GrowwMcp::ApiError => e
45
+ error_response(e)
46
+ end
47
+ end
48
+ end
49
+
50
+ class GetMargins < GrowwMcp::BaseTool
51
+ description "Get available margin and funds details including clear cash, collateral, " \
52
+ "equity margin (CNC/MIS), F&O margin (SPAN/exposure), and commodity margin."
53
+
54
+ input_schema(properties: {})
55
+
56
+ class << self
57
+ def call(server_context:)
58
+ client = server_context[:client]
59
+ result = client.margins
60
+ format_response(result)
61
+ rescue GrowwMcp::ApiError => e
62
+ error_response(e)
63
+ end
64
+ end
65
+ end
66
+
67
+ class CalculateMargin < GrowwMcp::BaseTool
68
+ description "Calculate required margin for one or more orders before placing them. " \
69
+ "Useful to check if you have sufficient funds."
70
+
71
+ input_schema(
72
+ properties: {
73
+ segment: {
74
+ type: "string",
75
+ enum: %w[CASH FNO],
76
+ description: "Market segment for the whole basket (sent as query param)",
77
+ },
78
+ orders: {
79
+ type: "array",
80
+ description: "Array of order objects to calculate margin for (sent as the JSON request body)",
81
+ items: {
82
+ type: "object",
83
+ properties: {
84
+ trading_symbol: { type: "string", description: "Stock/option symbol (e.g., RELIANCE, NIFTY24JUL25000CE)" },
85
+ transaction_type: { type: "string", enum: %w[BUY SELL], description: "Buy or sell" },
86
+ quantity: { type: "integer", description: "Number of shares/lots" },
87
+ price: { type: "number", description: "Limit price (for LIMIT/SL orders)" },
88
+ order_type: { type: "string", enum: %w[MARKET LIMIT SL SL_M], description: "Order type" },
89
+ product: { type: "string", enum: %w[CNC MIS NRML], description: "CNC=delivery, MIS=intraday, NRML=F&O" },
90
+ },
91
+ required: %w[trading_symbol transaction_type quantity order_type product],
92
+ },
93
+ },
94
+ },
95
+ required: %w[segment orders],
96
+ )
97
+
98
+ class << self
99
+ def call(server_context:, segment:, orders:)
100
+ client = server_context[:client]
101
+ result = client.calculate_margin(orders, segment: segment)
102
+ format_response(result)
103
+ rescue GrowwMcp::ApiError => e
104
+ error_response(e)
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end