Skip to content

Commit 8bdcadb

Browse files
committed
feat(tools): return structured client responses
1 parent dd93d9a commit 8bdcadb

File tree

8 files changed

+54
-43
lines changed

8 files changed

+54
-43
lines changed

src/schwab_mcp/tools/account.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44

55
from schwab_mcp.context import SchwabContext, SchwabServerContext
66
from schwab_mcp.tools.registry import register
7-
from schwab_mcp.tools.utils import call
7+
from schwab_mcp.tools.utils import JSONType, call
88

99

1010
@register
1111
async def get_account_numbers(
1212
ctx: SchwabContext,
13-
) -> str:
13+
) -> JSONType:
1414
"""
1515
Returns mapping of account IDs to account hashes. Hashes required for account-specific calls. Use first.
1616
"""
@@ -21,7 +21,7 @@ async def get_account_numbers(
2121
@register
2222
async def get_accounts(
2323
ctx: SchwabContext,
24-
) -> str:
24+
) -> JSONType:
2525
"""
2626
Returns balances/info for all linked accounts (funds, cash, margin). Does not return hashes; use get_account_numbers first.
2727
"""
@@ -32,7 +32,7 @@ async def get_accounts(
3232
@register
3333
async def get_accounts_with_positions(
3434
ctx: SchwabContext,
35-
) -> str:
35+
) -> JSONType:
3636
"""
3737
Returns balances, info, and positions (holdings, cost, gain/loss) for all linked accounts. Does not return hashes; use get_account_numbers first.
3838
"""
@@ -47,7 +47,7 @@ async def get_accounts_with_positions(
4747
async def get_account(
4848
ctx: SchwabContext,
4949
account_hash: Annotated[str, "Account hash for the Schwab account"],
50-
) -> str:
50+
) -> JSONType:
5151
"""
5252
Returns balance/info for a specific account via account_hash (from get_account_numbers). Includes funds, cash, margin info.
5353
"""
@@ -59,7 +59,7 @@ async def get_account(
5959
async def get_account_with_positions(
6060
ctx: SchwabContext,
6161
account_hash: Annotated[str, "Account hash for the Schwab account"],
62-
) -> str:
62+
) -> JSONType:
6363
"""
6464
Returns balance, info, and positions for a specific account via account_hash. Includes holdings, quantity, cost basis, unrealized gain/loss.
6565
"""
@@ -74,7 +74,7 @@ async def get_account_with_positions(
7474
@register
7575
async def get_user_preferences(
7676
ctx: SchwabContext,
77-
) -> str:
77+
) -> JSONType:
7878
"""
7979
Returns user preferences (nicknames, display settings, notifications) for all linked accounts.
8080
"""

src/schwab_mcp/tools/history.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from schwab_mcp.context import SchwabContext, SchwabServerContext
88
from schwab_mcp.tools.registry import register
9-
from schwab_mcp.tools.utils import call
9+
from schwab_mcp.tools.utils import JSONType, call
1010

1111

1212
def _parse_iso_datetime(value: str | None) -> datetime.datetime | None:
@@ -43,7 +43,7 @@ async def get_advanced_price_history(
4343
] = None,
4444
extended_hours: Annotated[bool | None, "Include extended hours data"] = None,
4545
previous_close: Annotated[bool | None, "Include previous close data"] = None,
46-
) -> str:
46+
) -> JSONType:
4747
"""
4848
Get price history with advanced period/frequency options. Specify period/frequency OR start/end datetimes.
4949
@@ -107,7 +107,7 @@ async def get_price_history_every_minute(
107107
] = None,
108108
extended_hours: Annotated[bool | None, "Include extended hours data"] = None,
109109
previous_close: Annotated[bool | None, "Include previous close data"] = None,
110-
) -> str:
110+
) -> JSONType:
111111
"""
112112
Get OHLCV price history per minute. For detailed intraday analysis. Max 48 days history. Dates ISO format.
113113
"""
@@ -139,7 +139,7 @@ async def get_price_history_every_five_minutes(
139139
] = None,
140140
extended_hours: Annotated[bool | None, "Include extended hours data"] = None,
141141
previous_close: Annotated[bool | None, "Include previous close data"] = None,
142-
) -> str:
142+
) -> JSONType:
143143
"""
144144
Get OHLCV price history per 5 minutes. Balance between detail and noise. Approx. 9 months history. Dates ISO format.
145145
"""
@@ -171,7 +171,7 @@ async def get_price_history_every_ten_minutes(
171171
] = None,
172172
extended_hours: Annotated[bool | None, "Include extended hours data"] = None,
173173
previous_close: Annotated[bool | None, "Include previous close data"] = None,
174-
) -> str:
174+
) -> JSONType:
175175
"""
176176
Get OHLCV price history per 10 minutes. Good for intraday trends/levels. Approx. 9 months history. Dates ISO format.
177177
"""
@@ -203,7 +203,7 @@ async def get_price_history_every_fifteen_minutes(
203203
] = None,
204204
extended_hours: Annotated[bool | None, "Include extended hours data"] = None,
205205
previous_close: Annotated[bool | None, "Include previous close data"] = None,
206-
) -> str:
206+
) -> JSONType:
207207
"""
208208
Get OHLCV price history per 15 minutes. Shows significant intraday moves, filters noise. Approx. 9 months history. Dates ISO format.
209209
"""
@@ -235,7 +235,7 @@ async def get_price_history_every_thirty_minutes(
235235
] = None,
236236
extended_hours: Annotated[bool | None, "Include extended hours data"] = None,
237237
previous_close: Annotated[bool | None, "Include previous close data"] = None,
238-
) -> str:
238+
) -> JSONType:
239239
"""
240240
Get OHLCV price history per 30 minutes. For broader intraday trends, filters noise. Approx. 9 months history. Dates ISO format.
241241
"""
@@ -273,7 +273,7 @@ async def get_price_history_every_day(
273273
previous_close: Annotated[
274274
bool | None, "Include the previous market day's closing price"
275275
] = None,
276-
) -> str:
276+
) -> JSONType:
277277
"""
278278
Get daily OHLCV price history. For medium/long-term analysis. Extensive history (back to 1985 possible). Dates ISO format.
279279
"""
@@ -305,7 +305,7 @@ async def get_price_history_every_week(
305305
] = None,
306306
extended_hours: Annotated[bool | None, "Include extended hours data"] = None,
307307
previous_close: Annotated[bool | None, "Include previous close data"] = None,
308-
) -> str:
308+
) -> JSONType:
309309
"""
310310
Get weekly OHLCV price history. For long-term analysis, major cycles. Extensive history (back to 1985 possible). Dates ISO format.
311311
"""

src/schwab_mcp/tools/options.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from schwab_mcp.context import SchwabContext, SchwabServerContext
88
from schwab_mcp.tools.registry import register
9-
from schwab_mcp.tools.utils import call
9+
from schwab_mcp.tools.utils import JSONType, call
1010

1111

1212
def _parse_date(value: str | datetime.date | None) -> datetime.date | None:
@@ -41,7 +41,7 @@ async def get_option_chain(
4141
str | datetime.date | None,
4242
"End date for option expiration ('YYYY-MM-DD' or datetime.date)",
4343
] = None,
44-
) -> str:
44+
) -> JSONType:
4545
"""
4646
Returns option chain data (strikes, expirations, prices) for a symbol. Use for standard chains.
4747
Params: symbol, contract_type (CALL/PUT/ALL), strike_count (default 25), include_quotes (bool), from_date (YYYY-MM-DD), to_date (YYYY-MM-DD).
@@ -117,7 +117,7 @@ async def get_advanced_option_chain(
117117
option_type: Annotated[
118118
str | None, "Filter option type: STANDARD, NON_STANDARD, ALL (default)"
119119
] = None,
120-
) -> str:
120+
) -> JSONType:
121121
"""
122122
Returns advanced option chain data with strategies, filters, and theoretical calculations. Use for complex analysis.
123123
Params: symbol, contract_type, strike_count, include_quotes, strategy (SINGLE/ANALYTICAL/etc.), interval, strike, strike_range (ITM/NTM/etc.), from/to_date, volatility/underlying_price/interest_rate/days_to_expiration (for ANALYTICAL), exp_month, option_type (STANDARD/NON_STANDARD/ALL).
@@ -160,7 +160,7 @@ async def get_advanced_option_chain(
160160
async def get_option_expiration_chain(
161161
ctx: SchwabContext,
162162
symbol: Annotated[str, "Symbol of the underlying security"],
163-
) -> str:
163+
) -> JSONType:
164164
"""
165165
Returns available option expiration dates for a symbol, without contract details. Lightweight call to find available cycles. Param: symbol.
166166
"""

src/schwab_mcp/tools/orders.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from schwab.orders.options import OptionSymbol
2828
from schwab_mcp.context import SchwabContext, SchwabServerContext
2929
from schwab_mcp.tools.registry import register
30-
from schwab_mcp.tools.utils import call
30+
from schwab_mcp.tools.utils import JSONType, call
3131

3232

3333
# Internal helper function to apply session and duration settings
@@ -170,7 +170,7 @@ async def get_order(
170170
ctx: SchwabContext,
171171
account_hash: Annotated[str, "Account hash for the Schwab account"],
172172
order_id: Annotated[str, "Order ID to get details for"],
173-
) -> str:
173+
) -> JSONType:
174174
"""
175175
Returns details for a specific order (ID, status, price, quantity, execution details). Params: account_hash, order_id.
176176
"""
@@ -195,7 +195,7 @@ async def get_orders(
195195
list[str] | str | None,
196196
"Filter by order status (e.g., WORKING, FILLED, CANCELED). See full list below.",
197197
] = None,
198-
) -> str:
198+
) -> JSONType:
199199
"""
200200
Returns order history for an account. Filter by date range (max 60 days past) and status.
201201
Params: account_hash, max_results, from_date (YYYY-MM-DD), to_date (YYYY-MM-DD), status (list/str).
@@ -239,7 +239,7 @@ async def cancel_order(
239239
ctx: SchwabContext,
240240
account_hash: Annotated[str, "Account hash for the Schwab account"],
241241
order_id: Annotated[str, "Order ID to cancel"],
242-
) -> str:
242+
) -> JSONType:
243243
"""
244244
Cancels a pending order. Cannot cancel executed/terminal orders. Params: account_hash, order_id. Returns cancellation request confirmation; check status after. *Write operation.*
245245
"""
@@ -267,7 +267,7 @@ async def place_equity_order(
267267
str | None,
268268
"Order duration: DAY (default), GOOD_TILL_CANCEL, FILL_OR_KILL (Limit/StopLimit only)",
269269
] = "DAY",
270-
) -> str:
270+
) -> JSONType:
271271
"""
272272
Places a single equity order (MARKET, LIMIT, STOP, STOP_LIMIT).
273273
Params: account_hash, symbol, quantity, instruction (BUY/SELL), order_type.
@@ -315,7 +315,7 @@ async def place_option_order(
315315
str | None,
316316
"Order duration: DAY (default), GOOD_TILL_CANCEL, FILL_OR_KILL (Limit only)",
317317
] = "DAY",
318-
) -> str:
318+
) -> JSONType:
319319
"""
320320
Places a single option order (MARKET, LIMIT).
321321
Params: account_hash, symbol, quantity, instruction (BUY_TO_OPEN/etc.), order_type.
@@ -426,7 +426,7 @@ async def place_one_cancels_other_order(
426426
second_order_spec: Annotated[
427427
dict, "Second order specification (dict from build_equity/option_order_spec)"
428428
],
429-
) -> str:
429+
) -> JSONType:
430430
"""
431431
Creates OCO order: execution of one cancels the other. Use for take-profit/stop-loss pairs.
432432
Params: account_hash, first_order_spec (dict), second_order_spec (dict).
@@ -460,7 +460,7 @@ async def place_first_triggers_second_order(
460460
dict,
461461
"Second (triggered) order specification (dict from build_equity/option_order_spec)",
462462
],
463-
) -> str:
463+
) -> JSONType:
464464
"""
465465
Creates conditional order: second order placed only after first executes. Use for activating exits after entry.
466466
Params: account_hash, first_order_spec (dict), second_order_spec (dict).
@@ -533,7 +533,7 @@ async def place_bracket_order(
533533
duration: Annotated[
534534
str | None, "Order duration: DAY (default), GOOD_TILL_CANCEL"
535535
] = "DAY",
536-
) -> str:
536+
) -> JSONType:
537537
"""
538538
Creates a bracket order: entry + OCO take-profit/stop-loss. Exits trigger after entry executes.
539539
Params: account_hash, symbol, quantity, entry_instruction (BUY/SELL), entry_type (MARKET/LIMIT/STOP/STOP_LIMIT), profit_price, loss_price.

src/schwab_mcp/tools/quotes.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from schwab_mcp.context import SchwabContext, SchwabServerContext
66
from schwab_mcp.tools.registry import register
7-
from schwab_mcp.tools.utils import call
7+
from schwab_mcp.tools.utils import JSONType, call
88

99

1010
@register
@@ -21,7 +21,7 @@ async def get_quotes(
2121
indicative: Annotated[
2222
bool | None, "True for indicative quotes (extended hours/futures)"
2323
] = None,
24-
) -> str:
24+
) -> JSONType:
2525
"""
2626
Returns current market quotes for specified symbols (stocks, ETFs, indices, options).
2727
Params: symbols (list or comma-separated string), fields (list/str: QUOTE/FUNDAMENTAL/etc.), indicative (bool).

src/schwab_mcp/tools/tools.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import datetime
66
from schwab_mcp.context import SchwabContext, SchwabServerContext
77
from schwab_mcp.tools.registry import register
8-
from schwab_mcp.tools.utils import call
8+
from schwab_mcp.tools.utils import JSONType, call
99

1010

1111
@register
@@ -27,7 +27,7 @@ async def get_market_hours(
2727
str | None,
2828
"Date ('YYYY-MM-DD', default today, max 1 year future)",
2929
] = None,
30-
) -> str:
30+
) -> JSONType:
3131
"""
3232
Get market hours for specified markets (EQUITY, OPTION, etc.) on a given date (YYYY-MM-DD, default today).
3333
"""
@@ -60,7 +60,7 @@ async def get_movers(
6060
frequency: Annotated[
6161
str | None, "Min % change threshold: ZERO, ONE, FIVE, TEN, THIRTY, SIXTY"
6262
] = None,
63-
) -> str:
63+
) -> JSONType:
6464
"""
6565
Get top 10 movers for an index/market (e.g., DJI, SPX, NASDAQ).
6666
Params: index, sort (VOLUME/TRADES/PERCENT_CHANGE_UP/DOWN), frequency (min % change: ZERO/ONE/etc.).
@@ -87,7 +87,7 @@ async def get_instruments(
8787
"DESCRIPTION_SEARCH, DESCRIPTION_REGEX, SEARCH, FUNDAMENTAL"
8888
),
8989
] = "symbol-search",
90-
) -> str:
90+
) -> JSONType:
9191
"""
9292
Search for instruments by symbol or description.
9393
Params: symbol (search term), projection (SYMBOL_SEARCH/SYMBOL_REGEX/etc., default symbol-search).

src/schwab_mcp/tools/transactions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from schwab_mcp.context import SchwabContext, SchwabServerContext
88
from schwab_mcp.tools.registry import register
9-
from schwab_mcp.tools.utils import call
9+
from schwab_mcp.tools.utils import JSONType, call
1010

1111

1212
@register
@@ -25,7 +25,7 @@ async def get_transactions(
2525
"Filter by type(s) (list/str): TRADE, DIVIDEND_OR_INTEREST, ACH_RECEIPT, etc. Default all.",
2626
] = None,
2727
symbol: Annotated[str | None, "Filter transactions by security symbol"] = None,
28-
) -> str:
28+
) -> JSONType:
2929
"""
3030
Get transaction history (trades, deposits, dividends, etc.) for an account. Filter by date range (max 60 days past), type, symbol.
3131
Params: account_hash, start_date (YYYY-MM-DD), end_date (YYYY-MM-DD), transaction_type (list/str: TRADE/DIVIDEND_OR_INTEREST/etc.), symbol.
@@ -67,7 +67,7 @@ async def get_transaction(
6767
ctx: SchwabContext,
6868
account_hash: Annotated[str, "Account hash for the Schwab account"],
6969
transaction_id: Annotated[str, "Transaction ID (from get_transactions)"],
70-
) -> str:
70+
) -> JSONType:
7171
"""
7272
Get detailed info for a specific transaction by ID.
7373
Params: account_hash, transaction_id (from get_transactions).

src/schwab_mcp/tools/utils.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
from __future__ import annotations
22

33
from collections.abc import Awaitable, Callable
4-
from typing import Any
4+
from typing import Any, TypeAlias
55

66

7-
async def call(func: Callable[..., Awaitable[Any]], *args: Any, **kwargs: Any) -> Any:
8-
"""Call a method on the Schwab client and return the response text."""
7+
JSONPrimitive = str | int | float | bool | None
8+
JSONType: TypeAlias = JSONPrimitive | dict[str, "JSONType"] | list["JSONType"]
9+
10+
11+
async def call(func: Callable[..., Awaitable[Any]], *args: Any, **kwargs: Any) -> JSONType:
12+
"""Call a Schwab client endpoint and return its JSON payload."""
913

1014
response = await func(*args, **kwargs)
1115
response.raise_for_status()
12-
return response.text
16+
17+
if getattr(response, "status_code", None) == 204:
18+
return None
19+
20+
try:
21+
return response.json()
22+
except ValueError as exc:
23+
raise ValueError("Expected JSON response from Schwab endpoint") from exc
1324

1425

15-
__all__ = ["call"]
26+
__all__ = ["call", "JSONType"]

0 commit comments

Comments
 (0)