magiqa-mcp 0.1.0__tar.gz

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,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: magiqa-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for MagiQa Options: covered calls, cash-secured puts, spreads, iron condors and options scanners for AI assistants
5
+ Author-email: Magiqa <info@magiqa.us>
6
+ License: MIT
7
+ Project-URL: Homepage, https://options.magiqa.us
8
+ Project-URL: Documentation, https://options.magiqa.us/documentation
9
+ Project-URL: Source, https://github.com/Avetik1980/optionsAPI
10
+ Keywords: mcp,options,covered calls,iron condor,stocks,finance,ai
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Financial and Insurance Industry
13
+ Classifier: Topic :: Office/Business :: Financial :: Investment
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: mcp<3,>=2.3
19
+ Requires-Dist: httpx>=0.27
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7; extra == "dev"
22
+
23
+ # magiqa-mcp
24
+
25
+ MCP server for [MagiQa Options](https://options.magiqa.us). Lets Claude, Cursor and other MCP clients screen US stock options with your MagiQa API key.
26
+
27
+ ## Tools
28
+
29
+ | Tool | What it returns |
30
+ |---|---|
31
+ | `get_stock_snapshot` | Price, SMA/RSI/MACD, signal, trend, valuation, earnings estimates |
32
+ | `find_covered_calls` | Covered calls, one symbol or market-wide |
33
+ | `find_cash_secured_puts` | Cash-secured puts |
34
+ | `find_credit_spreads` | Bull put (bullish) or bear call (bearish) spreads |
35
+ | `find_debit_spreads` | Bull call (bullish) or bear put (bearish) spreads |
36
+ | `find_iron_condors` | Short or long iron condors |
37
+ | `find_straddles_strangles` | Long or short straddles and strangles |
38
+ | `find_high_probability_options` | Highest probability-of-profit trades |
39
+ | `get_unusual_options_activity` | Volume far above open interest |
40
+ | `get_most_active_options` | Highest options volume |
41
+ | `get_highest_open_interest` | Largest open interest |
42
+ | `get_earnings_calendar` | Upcoming earnings |
43
+
44
+ Each tool returns at most 50 rows (default 10). Option premiums are from the previous session close. Results are analytics, not investment advice.
45
+
46
+ ## Setup
47
+
48
+ Get an API key at [options.magiqa.us](https://options.magiqa.us), then add the server to your MCP client.
49
+
50
+ **Claude Desktop** (`claude_desktop_config.json`) or **Cursor** (`.cursor/mcp.json`):
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "magiqa": {
56
+ "command": "uvx",
57
+ "args": ["magiqa-mcp"],
58
+ "env": { "MAGIQA_API_KEY": "your-api-key" }
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ Without `uv`: `pip install magiqa-mcp` and use `"command": "magiqa-mcp"` with no `args`.
65
+
66
+ Your plan's rate limits and symbol access apply.
@@ -0,0 +1,44 @@
1
+ # magiqa-mcp
2
+
3
+ MCP server for [MagiQa Options](https://options.magiqa.us). Lets Claude, Cursor and other MCP clients screen US stock options with your MagiQa API key.
4
+
5
+ ## Tools
6
+
7
+ | Tool | What it returns |
8
+ |---|---|
9
+ | `get_stock_snapshot` | Price, SMA/RSI/MACD, signal, trend, valuation, earnings estimates |
10
+ | `find_covered_calls` | Covered calls, one symbol or market-wide |
11
+ | `find_cash_secured_puts` | Cash-secured puts |
12
+ | `find_credit_spreads` | Bull put (bullish) or bear call (bearish) spreads |
13
+ | `find_debit_spreads` | Bull call (bullish) or bear put (bearish) spreads |
14
+ | `find_iron_condors` | Short or long iron condors |
15
+ | `find_straddles_strangles` | Long or short straddles and strangles |
16
+ | `find_high_probability_options` | Highest probability-of-profit trades |
17
+ | `get_unusual_options_activity` | Volume far above open interest |
18
+ | `get_most_active_options` | Highest options volume |
19
+ | `get_highest_open_interest` | Largest open interest |
20
+ | `get_earnings_calendar` | Upcoming earnings |
21
+
22
+ Each tool returns at most 50 rows (default 10). Option premiums are from the previous session close. Results are analytics, not investment advice.
23
+
24
+ ## Setup
25
+
26
+ Get an API key at [options.magiqa.us](https://options.magiqa.us), then add the server to your MCP client.
27
+
28
+ **Claude Desktop** (`claude_desktop_config.json`) or **Cursor** (`.cursor/mcp.json`):
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "magiqa": {
34
+ "command": "uvx",
35
+ "args": ["magiqa-mcp"],
36
+ "env": { "MAGIQA_API_KEY": "your-api-key" }
37
+ }
38
+ }
39
+ }
40
+ ```
41
+
42
+ Without `uv`: `pip install magiqa-mcp` and use `"command": "magiqa-mcp"` with no `args`.
43
+
44
+ Your plan's rate limits and symbol access apply.
@@ -0,0 +1,3 @@
1
+ """MagiQa Options MCP server: options strategy screeners for AI assistants."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,331 @@
1
+ """
2
+ MagiQa Options MCP server (stdio).
3
+
4
+ Thin wrapper over the MagiQa REST API using the caller's own API key
5
+ (MAGIQA_API_KEY). Plan limits and symbol access are enforced by the API.
6
+
7
+ Scanner fields that are estimated rather than measured (IV rank/percentile,
8
+ historical volatility, OI change) are stripped from every response until
9
+ they are backed by stored history.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from typing import Any, Literal, Optional
15
+
16
+ import httpx
17
+ from mcp.server import MCPServer
18
+ from mcp.server.mcpserver.exceptions import ToolError
19
+
20
+ from magiqa_mcp import __version__
21
+
22
+ DEFAULT_BASE_URL = "https://options.magiqa.us"
23
+ DEFAULT_LIMIT = 10
24
+ MAX_LIMIT = 50
25
+ TIMEOUT_SECONDS = 30.0
26
+
27
+ ESTIMATED_FIELDS = frozenset({
28
+ "iv_rank", "iv_percentile", "iv_high_52w", "iv_low_52w", "iv_signal", "strategy_hint",
29
+ "iv_hv", "iv_hv_ratio", "historical_volatility", "his_vol_30d",
30
+ "previous_oi", "oi_change", "oi_change_pct",
31
+ })
32
+
33
+ STOCK_SNAPSHOT_FIELDS = (
34
+ "symbol", "name", "sector", "price", "change", "change_percent", "open", "high", "low",
35
+ "volume_raw", "avg_volume_raw", "market_cap", "pe_ratio", "dividend_yield", "one_year_return",
36
+ "sma_5", "sma_8", "sma_13", "sma_50", "sma_200", "rsi", "macd", "macd_signal", "volatility",
37
+ "signal", "sma_decision", "trend", "altman_z_score", "piotroski_score",
38
+ "eps_actual", "eps_estimated", "revenue_actual", "revenue_estimated",
39
+ "quote_year_high", "quote_year_low", "last_updated", "quote_refreshed_at",
40
+ )
41
+
42
+ INSTRUCTIONS = (
43
+ "MagiQa screens US stock options (S&P 500 + popular ETFs) and returns strategy-ready trades "
44
+ "with probability of profit, breakevens and returns. Option premiums come from the previous "
45
+ "session close. Results are analytics, not investment advice."
46
+ )
47
+
48
+
49
+ class MagiqaAPIError(ToolError):
50
+ """Anticipated API failure; the SDK shows this message to the model."""
51
+
52
+
53
+ class MagiqaClient:
54
+ def __init__(self, api_key: Optional[str] = None, base_url: Optional[str] = None,
55
+ transport: Optional[httpx.BaseTransport] = None):
56
+ self.api_key = api_key or os.getenv("MAGIQA_API_KEY", "")
57
+ self.base_url = (base_url or os.getenv("MAGIQA_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
58
+ self._http = httpx.Client(
59
+ base_url=self.base_url,
60
+ headers={"X-API-Key": self.api_key, "User-Agent": f"magiqa-mcp/{__version__}"},
61
+ timeout=TIMEOUT_SECONDS,
62
+ transport=transport,
63
+ )
64
+
65
+ def get(self, path: str, params: Optional[dict] = None) -> Any:
66
+ if not self.api_key:
67
+ raise MagiqaAPIError(
68
+ "MAGIQA_API_KEY is not set. Get a key at https://options.magiqa.us and add it to "
69
+ "the MCP server's environment."
70
+ )
71
+ clean = {k: v for k, v in (params or {}).items() if v is not None}
72
+ try:
73
+ resp = self._http.get(path, params=clean)
74
+ except httpx.HTTPError as e:
75
+ raise MagiqaAPIError(f"Could not reach MagiQa API: {e}") from e
76
+ if resp.status_code == 401:
77
+ raise MagiqaAPIError(_unauthorized_message(resp))
78
+ if resp.status_code == 403:
79
+ raise MagiqaAPIError(f"Not available on your MagiQa plan (403): {_detail(resp)}")
80
+ if resp.status_code == 429:
81
+ raise MagiqaAPIError("MagiQa rate limit reached (429). Try again later or upgrade the plan.")
82
+ if resp.status_code >= 400:
83
+ raise MagiqaAPIError(f"MagiQa API error {resp.status_code}: {_detail(resp)}")
84
+ return resp.json()
85
+
86
+
87
+ def _unauthorized_message(resp: httpx.Response) -> str:
88
+ """A 401 is either a bad key or an endpoint that still accepts RapidAPI only."""
89
+ detail = _detail(resp)
90
+ lowered = detail.lower()
91
+ if "rapidapi" in lowered or "x-rapidapi" in lowered:
92
+ return (
93
+ "This MagiQa endpoint is not available with an API key (401). "
94
+ "It only accepts RapidAPI traffic."
95
+ )
96
+ if detail and detail not in ("nope", "{}"):
97
+ return f"MagiQa rejected the API key (401): {detail}"
98
+ return "MagiQa rejected the API key (401). Check MAGIQA_API_KEY."
99
+
100
+
101
+ def _detail(resp: httpx.Response) -> str:
102
+ try:
103
+ body = resp.json()
104
+ return str(body.get("detail", body))[:300] if isinstance(body, dict) else str(body)[:300]
105
+ except ValueError:
106
+ return resp.text[:300]
107
+
108
+
109
+ def _clamp(limit: int) -> int:
110
+ return max(1, min(int(limit), MAX_LIMIT))
111
+
112
+
113
+ def shape(payload: Any, limit: int) -> Any:
114
+ """Drop estimated fields at any depth and cap every list at `limit` rows."""
115
+ if isinstance(payload, dict):
116
+ out = {}
117
+ for k, v in payload.items():
118
+ if k in ESTIMATED_FIELDS:
119
+ continue
120
+ if isinstance(v, list) and len(v) > limit:
121
+ out[k] = [shape(x, limit) for x in v[:limit]]
122
+ out[f"{k}_total"] = len(v)
123
+ else:
124
+ out[k] = shape(v, limit)
125
+ return out
126
+ if isinstance(payload, list):
127
+ return [shape(x, limit) for x in payload[:limit]]
128
+ return payload
129
+
130
+
131
+ def build_server(client: Optional[MagiqaClient] = None) -> MCPServer:
132
+ api = client or MagiqaClient()
133
+ mcp = MCPServer(
134
+ "MagiQa Options",
135
+ instructions=INSTRUCTIONS,
136
+ version=__version__,
137
+ website_url="https://options.magiqa.us",
138
+ )
139
+
140
+ def fetch(path: str, limit: int, params: Optional[dict] = None) -> Any:
141
+ n = _clamp(limit)
142
+ return shape(api.get(path, {**(params or {}), "limit": n}), n)
143
+
144
+ @mcp.tool()
145
+ def get_stock_snapshot(symbol: str) -> dict:
146
+ """Price, technical indicators (SMA, RSI, MACD, volatility, signal, trend), valuation and
147
+ upcoming earnings estimates for one US stock or ETF."""
148
+ data = api.get(f"/api/stocks/{symbol.upper().strip()}")
149
+ if not isinstance(data, dict):
150
+ return {"symbol": symbol.upper(), "data": data}
151
+ return {k: data.get(k) for k in STOCK_SNAPSHOT_FIELDS if k in data}
152
+
153
+ @mcp.tool()
154
+ def find_covered_calls(
155
+ symbol: Optional[str] = None,
156
+ min_days_to_expiration: int = 0,
157
+ max_days_to_expiration: int = 60,
158
+ min_probability: float = 0,
159
+ min_return: float = 0,
160
+ limit: int = DEFAULT_LIMIT,
161
+ ) -> Any:
162
+ """Covered call opportunities (own 100 shares, sell a call). With `symbol`, returns that
163
+ symbol's covered calls; without it, the best across the market using the filters."""
164
+ if symbol:
165
+ return fetch(f"/api/basic-options/covered-calls/{symbol.upper().strip()}", limit)
166
+ return fetch("/api/basic-options/covered-calls", limit, {
167
+ "minDaysToExpiration": min_days_to_expiration,
168
+ "maxDaysToExpiration": max_days_to_expiration,
169
+ "minProbability": min_probability,
170
+ "minReturn": min_return,
171
+ })
172
+
173
+ @mcp.tool()
174
+ def find_cash_secured_puts(
175
+ symbol: Optional[str] = None,
176
+ min_days_to_expiration: Optional[int] = None,
177
+ max_days_to_expiration: Optional[int] = None,
178
+ min_profit_probability: Optional[float] = None,
179
+ min_annualized_return: Optional[float] = None,
180
+ max_investment: Optional[float] = None,
181
+ limit: int = DEFAULT_LIMIT,
182
+ ) -> Any:
183
+ """Cash-secured (naked) put opportunities: sell a put, collect premium, possibly buy the
184
+ stock at the strike. Optional symbol and filters."""
185
+ return fetch("/api/basic-options/naked-puts", limit, {
186
+ "symbol": symbol.upper().strip() if symbol else None,
187
+ "minDaysToExpiration": min_days_to_expiration,
188
+ "maxDaysToExpiration": max_days_to_expiration,
189
+ "minProfitProbability": min_profit_probability,
190
+ "minAnnualizedReturn": min_annualized_return,
191
+ "maxInvestment": max_investment,
192
+ })
193
+
194
+ @mcp.tool()
195
+ def find_credit_spreads(
196
+ direction: Literal["bullish", "bearish"],
197
+ min_days_to_expiration: Optional[int] = None,
198
+ max_days_to_expiration: Optional[int] = None,
199
+ min_profit_probability: Optional[float] = None,
200
+ min_roi: Optional[float] = None,
201
+ max_investment: Optional[float] = None,
202
+ limit: int = DEFAULT_LIMIT,
203
+ ) -> Any:
204
+ """Credit spreads: bullish = bull put spread, bearish = bear call spread. Defined risk,
205
+ collect premium up front."""
206
+ path = "/api/spreads/bull-put" if direction == "bullish" else "/api/spreads/bear-call"
207
+ return fetch(path, limit, {
208
+ "minDaysToExpiration": min_days_to_expiration,
209
+ "maxDaysToExpiration": max_days_to_expiration,
210
+ "minProfitProbability": min_profit_probability,
211
+ "minROI": min_roi,
212
+ "maxInvestment": max_investment,
213
+ })
214
+
215
+ @mcp.tool()
216
+ def find_debit_spreads(
217
+ direction: Literal["bullish", "bearish"],
218
+ min_days_to_expiration: Optional[int] = None,
219
+ max_days_to_expiration: Optional[int] = None,
220
+ min_profit_probability: Optional[float] = None,
221
+ min_roi: Optional[float] = None,
222
+ max_investment: Optional[float] = None,
223
+ limit: int = DEFAULT_LIMIT,
224
+ ) -> Any:
225
+ """Debit spreads: bullish = bull call spread, bearish = bear put spread. Defined risk,
226
+ pay premium up front."""
227
+ path = "/api/spreads/bull-call" if direction == "bullish" else "/api/spreads/bear-put"
228
+ return fetch(path, limit, {
229
+ "minDaysToExpiration": min_days_to_expiration,
230
+ "maxDaysToExpiration": max_days_to_expiration,
231
+ "minProfitProbability": min_profit_probability,
232
+ "minROI": min_roi,
233
+ "maxInvestment": max_investment,
234
+ })
235
+
236
+ @mcp.tool()
237
+ def find_iron_condors(
238
+ side: Literal["short", "long"] = "short",
239
+ min_days_to_expiration: int = 0,
240
+ max_days_to_expiration: int = 180,
241
+ min_profit_probability: float = 0,
242
+ limit: int = DEFAULT_LIMIT,
243
+ ) -> Any:
244
+ """Iron condors. short = sell the condor for a credit (profits if the stock stays in a
245
+ range); long = buy it (profits on a big move)."""
246
+ return fetch("/api/scanners/iron-condor-screener", limit, {
247
+ "strategy_type": f"{side}_iron_condor",
248
+ "minDaysToExpiration": min_days_to_expiration,
249
+ "maxDaysToExpiration": max_days_to_expiration,
250
+ "minProbProfit": min_profit_probability,
251
+ })
252
+
253
+ @mcp.tool()
254
+ def find_straddles_strangles(
255
+ kind: Literal["straddle", "strangle"] = "straddle",
256
+ side: Literal["long", "short"] = "long",
257
+ min_days_to_expiration: int = 0,
258
+ max_days_to_expiration: int = 90,
259
+ min_profit_probability: float = 0,
260
+ min_volume: int = 0,
261
+ limit: int = DEFAULT_LIMIT,
262
+ ) -> Any:
263
+ """Straddles (same strike) or strangles (different strikes). long = bet on a big move;
264
+ short = bet on a quiet market."""
265
+ return fetch(f"/api/scanners/{kind}-screener", limit, {
266
+ "strategy_type": side,
267
+ "minDaysToExpiration": min_days_to_expiration,
268
+ "maxDaysToExpiration": max_days_to_expiration,
269
+ "minProbProfit": min_profit_probability,
270
+ "minVolume": min_volume,
271
+ })
272
+
273
+ @mcp.tool()
274
+ def find_high_probability_options(
275
+ min_profit_probability: float = 60,
276
+ max_days_to_expiration: int = 45,
277
+ min_volume: int = 100,
278
+ limit: int = DEFAULT_LIMIT,
279
+ ) -> Any:
280
+ """Option trades with the highest probability of profit across the market."""
281
+ return fetch("/api/scanners/options-screener/high-probability", limit, {
282
+ "minProfitProb": min_profit_probability,
283
+ "maxDaysToExpiration": max_days_to_expiration,
284
+ "minVolume": min_volume,
285
+ })
286
+
287
+ @mcp.tool()
288
+ def get_unusual_options_activity(
289
+ symbol: Optional[str] = None,
290
+ option_type: Literal["all", "call", "put"] = "all",
291
+ min_volume: int = 1000,
292
+ max_days_to_expiration: int = 45,
293
+ limit: int = DEFAULT_LIMIT,
294
+ ) -> Any:
295
+ """Contracts trading far above their open interest (possible new positioning)."""
296
+ if symbol:
297
+ return fetch(f"/api/scanners/unusual-activity/{symbol.upper().strip()}", limit)
298
+ return fetch("/api/scanners/unusual-activity", limit, {
299
+ "optionType": option_type,
300
+ "minVolume": min_volume,
301
+ "maxDaysToExpiration": max_days_to_expiration,
302
+ })
303
+
304
+ @mcp.tool()
305
+ def get_most_active_options(symbol: Optional[str] = None, limit: int = DEFAULT_LIMIT) -> Any:
306
+ """Underlyings (or one symbol's contracts) with the highest options volume."""
307
+ if symbol:
308
+ return fetch(f"/api/scanners/most-active-options/{symbol.upper().strip()}", limit)
309
+ return fetch("/api/scanners/most-active-options", limit)
310
+
311
+ @mcp.tool()
312
+ def get_highest_open_interest(symbol: Optional[str] = None, limit: int = DEFAULT_LIMIT) -> Any:
313
+ """Underlyings (or one symbol's contracts) with the largest open interest."""
314
+ if symbol:
315
+ return fetch(f"/api/scanners/highest-open-interest/{symbol.upper().strip()}", limit)
316
+ return fetch("/api/scanners/highest-open-interest", limit)
317
+
318
+ @mcp.tool()
319
+ def get_earnings_calendar(days: int = 7) -> Any:
320
+ """Upcoming earnings reports for the next `days` days (max 30)."""
321
+ return shape(api.get("/api/stocks/earnings-calendar", {"days": max(1, min(days, 30))}), 200)
322
+
323
+ return mcp
324
+
325
+
326
+ def main() -> None:
327
+ build_server().run("stdio")
328
+
329
+
330
+ if __name__ == "__main__":
331
+ main()
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: magiqa-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for MagiQa Options: covered calls, cash-secured puts, spreads, iron condors and options scanners for AI assistants
5
+ Author-email: Magiqa <info@magiqa.us>
6
+ License: MIT
7
+ Project-URL: Homepage, https://options.magiqa.us
8
+ Project-URL: Documentation, https://options.magiqa.us/documentation
9
+ Project-URL: Source, https://github.com/Avetik1980/optionsAPI
10
+ Keywords: mcp,options,covered calls,iron condor,stocks,finance,ai
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Financial and Insurance Industry
13
+ Classifier: Topic :: Office/Business :: Financial :: Investment
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: mcp<3,>=2.3
19
+ Requires-Dist: httpx>=0.27
20
+ Provides-Extra: dev
21
+ Requires-Dist: pytest>=7; extra == "dev"
22
+
23
+ # magiqa-mcp
24
+
25
+ MCP server for [MagiQa Options](https://options.magiqa.us). Lets Claude, Cursor and other MCP clients screen US stock options with your MagiQa API key.
26
+
27
+ ## Tools
28
+
29
+ | Tool | What it returns |
30
+ |---|---|
31
+ | `get_stock_snapshot` | Price, SMA/RSI/MACD, signal, trend, valuation, earnings estimates |
32
+ | `find_covered_calls` | Covered calls, one symbol or market-wide |
33
+ | `find_cash_secured_puts` | Cash-secured puts |
34
+ | `find_credit_spreads` | Bull put (bullish) or bear call (bearish) spreads |
35
+ | `find_debit_spreads` | Bull call (bullish) or bear put (bearish) spreads |
36
+ | `find_iron_condors` | Short or long iron condors |
37
+ | `find_straddles_strangles` | Long or short straddles and strangles |
38
+ | `find_high_probability_options` | Highest probability-of-profit trades |
39
+ | `get_unusual_options_activity` | Volume far above open interest |
40
+ | `get_most_active_options` | Highest options volume |
41
+ | `get_highest_open_interest` | Largest open interest |
42
+ | `get_earnings_calendar` | Upcoming earnings |
43
+
44
+ Each tool returns at most 50 rows (default 10). Option premiums are from the previous session close. Results are analytics, not investment advice.
45
+
46
+ ## Setup
47
+
48
+ Get an API key at [options.magiqa.us](https://options.magiqa.us), then add the server to your MCP client.
49
+
50
+ **Claude Desktop** (`claude_desktop_config.json`) or **Cursor** (`.cursor/mcp.json`):
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "magiqa": {
56
+ "command": "uvx",
57
+ "args": ["magiqa-mcp"],
58
+ "env": { "MAGIQA_API_KEY": "your-api-key" }
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ Without `uv`: `pip install magiqa-mcp` and use `"command": "magiqa-mcp"` with no `args`.
65
+
66
+ Your plan's rate limits and symbol access apply.
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ magiqa_mcp/__init__.py
4
+ magiqa_mcp/server.py
5
+ magiqa_mcp.egg-info/PKG-INFO
6
+ magiqa_mcp.egg-info/SOURCES.txt
7
+ magiqa_mcp.egg-info/dependency_links.txt
8
+ magiqa_mcp.egg-info/entry_points.txt
9
+ magiqa_mcp.egg-info/requires.txt
10
+ magiqa_mcp.egg-info/top_level.txt
11
+ tests/test_server.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ magiqa-mcp = magiqa_mcp.server:main
@@ -0,0 +1,5 @@
1
+ mcp<3,>=2.3
2
+ httpx>=0.27
3
+
4
+ [dev]
5
+ pytest>=7
@@ -0,0 +1 @@
1
+ magiqa_mcp
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "magiqa-mcp"
7
+ dynamic = ["version"]
8
+ description = "MCP server for MagiQa Options: covered calls, cash-secured puts, spreads, iron condors and options scanners for AI assistants"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Magiqa", email = "info@magiqa.us" }]
13
+ keywords = ["mcp", "options", "covered calls", "iron condor", "stocks", "finance", "ai"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Financial and Insurance Industry",
17
+ "Topic :: Office/Business :: Financial :: Investment",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ ]
21
+ dependencies = [
22
+ "mcp>=2.3,<3",
23
+ "httpx>=0.27",
24
+ ]
25
+
26
+ [project.optional-dependencies]
27
+ dev = ["pytest>=7"]
28
+
29
+ [project.scripts]
30
+ magiqa-mcp = "magiqa_mcp.server:main"
31
+
32
+ [project.urls]
33
+ Homepage = "https://options.magiqa.us"
34
+ Documentation = "https://options.magiqa.us/documentation"
35
+ Source = "https://github.com/Avetik1980/optionsAPI"
36
+
37
+ [tool.setuptools]
38
+ packages = ["magiqa_mcp"]
39
+
40
+ [tool.setuptools.dynamic]
41
+ version = { attr = "magiqa_mcp.__version__" }
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,127 @@
1
+ """In-memory tests for magiqa-mcp against a mocked MagiQa API (no network)."""
2
+ from __future__ import annotations
3
+
4
+ import asyncio
5
+ import json
6
+
7
+ import httpx
8
+ import pytest
9
+ from mcp import Client
10
+
11
+ from magiqa_mcp.server import MAX_LIMIT, MagiqaClient, build_server, shape
12
+
13
+ EXPECTED_TOOLS = {
14
+ "get_stock_snapshot", "find_covered_calls", "find_cash_secured_puts", "find_credit_spreads",
15
+ "find_debit_spreads", "find_iron_condors", "find_straddles_strangles",
16
+ "find_high_probability_options", "get_unusual_options_activity", "get_most_active_options",
17
+ "get_highest_open_interest", "get_earnings_calendar",
18
+ }
19
+
20
+
21
+ def make_server(handler, api_key="test-key"):
22
+ seen = []
23
+
24
+ def wrapped(request: httpx.Request) -> httpx.Response:
25
+ seen.append(request)
26
+ return handler(request)
27
+
28
+ client = MagiqaClient(api_key=api_key, base_url="https://api.test",
29
+ transport=httpx.MockTransport(wrapped))
30
+ return build_server(client), seen
31
+
32
+
33
+ def call(server, name, args):
34
+ async def run():
35
+ async with Client(server) as c:
36
+ return await c.call_tool(name, args)
37
+ return asyncio.run(run())
38
+
39
+
40
+ def payload(result):
41
+ if result.structured_content is not None:
42
+ sc = result.structured_content
43
+ return sc.get("result", sc) if isinstance(sc, dict) and set(sc) == {"result"} else sc
44
+ return json.loads(result.content[0].text)
45
+
46
+
47
+ def test_lists_exactly_the_v1_tools():
48
+ server, _ = make_server(lambda r: httpx.Response(200, json={}))
49
+
50
+ async def run():
51
+ async with Client(server) as c:
52
+ return await c.list_tools()
53
+ tools = {t.name for t in asyncio.run(run()).tools}
54
+ assert tools == EXPECTED_TOOLS
55
+
56
+
57
+ def test_shape_strips_estimated_fields_and_caps_lists():
58
+ data = {"count": 3, "data": [{"symbol": "A", "iv_rank": 50, "pop": 70}] * 3}
59
+ out = shape(data, 2)
60
+ assert len(out["data"]) == 2 and out["data_total"] == 3
61
+ assert "iv_rank" not in out["data"][0] and out["data"][0]["pop"] == 70
62
+
63
+
64
+ def test_covered_calls_sends_key_filters_and_clamped_limit():
65
+ server, seen = make_server(lambda r: httpx.Response(200, json={"data": [{"symbol": "AAPL"}]}))
66
+ res = call(server, "find_covered_calls", {"max_days_to_expiration": 45, "limit": 500})
67
+ assert not res.is_error
68
+ req = seen[-1]
69
+ assert req.url.path == "/api/basic-options/covered-calls"
70
+ assert req.headers["X-API-Key"] == "test-key"
71
+ assert req.url.params["maxDaysToExpiration"] == "45"
72
+ assert req.url.params["limit"] == str(MAX_LIMIT)
73
+
74
+
75
+ def test_covered_calls_with_symbol_uses_symbol_route():
76
+ server, seen = make_server(lambda r: httpx.Response(200, json={"data": []}))
77
+ call(server, "find_covered_calls", {"symbol": "aapl"})
78
+ assert seen[-1].url.path == "/api/basic-options/covered-calls/AAPL"
79
+
80
+
81
+ def test_credit_spread_direction_routes():
82
+ server, seen = make_server(lambda r: httpx.Response(200, json={"data": []}))
83
+ call(server, "find_credit_spreads", {"direction": "bearish"})
84
+ assert seen[-1].url.path == "/api/spreads/bear-call"
85
+ call(server, "find_credit_spreads", {"direction": "bullish", "min_roi": 10})
86
+ assert seen[-1].url.path == "/api/spreads/bull-put"
87
+ assert seen[-1].url.params["minROI"] == "10.0"
88
+ assert "maxInvestment" not in seen[-1].url.params
89
+
90
+
91
+ def test_iron_condor_side_maps_to_strategy_type():
92
+ server, seen = make_server(lambda r: httpx.Response(200, json={"data": []}))
93
+ call(server, "find_iron_condors", {"side": "short"})
94
+ assert seen[-1].url.params["strategy_type"] == "short_iron_condor"
95
+
96
+
97
+ def test_stock_snapshot_returns_curated_fields_only():
98
+ row = {"symbol": "NVDA", "price": 239.24, "sma_50": 219.77, "balance_total_assets": 1, "iv_rank": 9}
99
+ server, _ = make_server(lambda r: httpx.Response(200, json=row))
100
+ out = payload(call(server, "get_stock_snapshot", {"symbol": "nvda"}))
101
+ assert out == {"symbol": "NVDA", "price": 239.24, "sma_50": 219.77}
102
+
103
+
104
+ @pytest.mark.parametrize("status,needle", [(401, "API key"), (403, "plan"), (429, "rate limit")])
105
+ def test_api_errors_become_readable_tool_errors(status, needle):
106
+ server, _ = make_server(lambda r: httpx.Response(status, json={"detail": "nope"}))
107
+ res = call(server, "get_most_active_options", {})
108
+ assert res.is_error
109
+ assert needle in res.content[0].text
110
+
111
+
112
+ def test_rapidapi_only_endpoint_is_not_blamed_on_the_key():
113
+ server, _ = make_server(lambda r: httpx.Response(
114
+ 401, json={"detail": "Missing RapidAPI header. X-RapidAPI-Host is required."}
115
+ ))
116
+ res = call(server, "get_most_active_options", {})
117
+ assert res.is_error
118
+ text = res.content[0].text
119
+ assert "not available with an API key" in text
120
+ assert "Check MAGIQA_API_KEY" not in text
121
+
122
+
123
+ def test_missing_api_key_is_explained_without_calling_api():
124
+ server, seen = make_server(lambda r: httpx.Response(200, json={}), api_key="")
125
+ res = call(server, "get_earnings_calendar", {})
126
+ assert res.is_error and "MAGIQA_API_KEY" in res.content[0].text
127
+ assert seen == []