Metadata-Version: 2.4
Name: ballstreet-mcp
Version: 1.0.1
Summary: MCP server for the BallStreet fantasy-football market API
Author: Ball Street League
License-Expression: MIT
Project-URL: Homepage, https://ballstreetleague.com
Project-URL: Documentation, https://github.com/jhosic/BallStreet/tree/main/ballstreet_mcp
Project-URL: Issues, https://github.com/jhosic/BallStreet/issues
Keywords: mcp,model-context-protocol,fantasy-football,trading,ballstreet
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Games/Entertainment
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp>=1.2.0
Requires-Dist: httpx>=0.27
Dynamic: license-file

# BallStreet MCP Server

Exposes the BallStreet API to MCP clients (Claude Code, Claude Desktop, or
anything else that speaks MCP), so a model can explain the game, read your
league, manage a portfolio, and place orders.

Two distinct uses, and most people want the first:

- **Ask questions about your league.** A read-only key lets an assistant answer
  "why did my player drop overnight", "who is locked this week", "how many
  shares are left" using your actual numbers, and read the official rules from
  the server rather than guessing from how stock markets usually work.
- **Run a trading bot.** A key with `trade` can place and manage orders.

The same server, with a `debug` key, is what we use to diagnose a live league
from a terminal — see [Debugging with it](#debugging-with-it).

## Setup

1. Create a key at <https://ballstreetleague.com/dashboard/api-keys/>.
   Pick the narrowest scopes that do the job, and scope it to one league if
   the bot only plays in one.

2. Install it:

   ```bash
   pip install ballstreet-mcp
   ```

   Dependencies are just `mcp` and `httpx`. The package does not import Django
   and does not need a running BallStreet server or database — it is an HTTP
   client, so it runs anywhere. Python 3.10 or newer.

3. Register it:

   ```bash
   claude mcp add ballstreet \
     --env BALLSTREET_API_KEY=bsl_xxxxxxxx_... \
     -- ballstreet-mcp
   ```

   For Claude Desktop, in `claude_desktop_config.json`:

   ```json
   {
     "mcpServers": {
       "ballstreet": {
         "command": "ballstreet-mcp",
         "env": { "BALLSTREET_API_KEY": "bsl_xxxxxxxx_..." }
       }
     }
   }
   ```

## Configuration

| Variable | Default | Purpose |
| --- | --- | --- |
| `BALLSTREET_API_KEY` | — | Required. |
| `BALLSTREET_BASE_URL` | `https://ballstreetleague.com` | Point at a local server for development. |
| `BALLSTREET_READ_ONLY` | unset | `1` withholds the trading tools even if the key allows them. |
| `BALLSTREET_MAX_SHARES_PER_ORDER` | unset | Client-side per-order share cap. |

`BALLSTREET_READ_ONLY` and the share cap exist so you can bound a strategy you
are still experimenting with, without revoking and reissuing the key. They are
convenience rails on the client; the server's scopes are the real boundary.

## Tools

Tools are registered from what the key actually allows, discovered at startup.
A read-only key does not merely get refused when it calls `place_order` — the
tool is never offered, so the model does not plan around a capability it does
not have.

**Always available:** `whoami`, `market_primer`, `get_rules`, `search_rules`

**`read`:** `get_league_rules`, `list_leagues`, `get_market_state`, `list_players`, `get_player`,
`get_price_history`, `get_player_news`, `get_holdings_news`,
`get_player_holders`, `get_matchups`, `get_portfolio`, `get_portfolio_history`,
`get_leaderboard`, `get_live_stats`, `list_orders`, `get_pending_orders`

**`trade`:** `place_order`, `cancel_order`, `reorder_pending_orders`

**`debug`** (tester accounts only): `debug_league`, `debug_player`

## Answering questions about the game

`get_rules` returns the official rules, glossary, and FAQ **from the server**,
with every constant filled in from the live engine settings — so it cannot
quote a number that has since been retuned. `search_rules` ranks them against a
specific question ("why can't I buy him", "what is a short").

`get_league_rules` is the one that matters most for support answers. The
universal rules have to hedge — demand pricing is optional, round counts are
configurable — and this resolves them against what the league is actually set
to. Answering "why did his price go up" without it risks describing a demand
premium in a league that has demand switched off.

The rules endpoint is public, so an assistant can explain the game to someone
who does not have an account yet.

## What a bot should know about this market

`market_primer` is fetched from the server at startup. The short version,
because it is genuinely different from the order-book markets most trading code
assumes:

- **Trading does not move prices.** No order book, no per-trade impact.
  Prices move at settlement and on projection updates.
- **Speed is not an edge.** Buys queue and fill in a batch run ordered by a
  per-member priority that *rotates* each cycle. An order placed three seconds
  after an injury report fills at the same price as one placed three hours
  after it. Polling every second buys you nothing but a rate limit.
- **Float is finite.** `league_size x 100` shares per player. When it is gone,
  buys are refused. This is the real scarcity — and the one place ordering
  matters, via your own `fill_priority` within your queue.
- **Halted players cannot be bought.** IR/OUT/inactive. You can always sell or
  cover.
- **Performance is measured against projections**, not raw points.

## Disclosure

Placing an order through an API key marks your league membership
`bot_operated`, which your leaguemates can see. Bots are a supported way to
play; undisclosed ones are not.

## Debugging with it

A key with the `debug` scope (tester accounts only) adds two read-only tools
that expose what no screen shows:

- `debug_league` — market state, *both* week counters, and the fill queue
  ordered by `settlement_priority`. This is what answers "why did my order not
  fill" and "why is it showing the wrong week".
- `debug_player` — every component of a price: base, each multiplier, the
  share basis the posted price was built from, your own buy and sell prices,
  and recent price rows with their components.

`debug_player` also evaluates the two invariants server-side, so a diagnosis
never depends on a client reassembling the arithmetic:

- `sell_never_above_buy` — a hard invariant. False means a real bug.
- `posted_matches_recomputed` — informational only. The posted price
  deliberately lags its own share basis between settlements, so a mismatch
  mid-week is expected on any player carrying demand.

There is no write path in the debug surface, by design. Diagnosing a pricing
question needs reads, and a credential that can mutate a live league while
investigating it is a category of accident worth designing out.

## Releasing

`./release-mcp.sh patch` from the repo root. See [RELEASING.md](RELEASING.md).

## Rate limits

120 read requests/minute and 30 writes/minute per key, metered per key rather
than per user so one bot cannot starve another integration. A `429` comes back
with `Retry-After`.
