Skip to main content
Glama
jigonyoo

odds-mcp

by jigonyoo
README.md
# odds-mcp

**An MCP server over odds data where every read names its evidence and
the one write is impossible without an approval token.**

[Model Context Protocol](https://modelcontextprotocol.io) is how an LLM
agent gets tools. The dangerous part is never the reads - it is the one
tool that *does* something. This server demonstrates the pattern that
makes side-effectful agent tools safe enough to exist: a risk envelope
checked before a human ever sees the request, a single-use approval
token issued by a separate step, a publish that cannot happen without
it, and an append-only audit log that records every attempt -
especially the refused ones.

Data comes from [odds-consensus](https://github.com/jigonyoo/odds-consensus):
5 books normalized into one schema, de-margined fair prices, and a
cross-book consensus view. This repo is the integration surface on top.

```
MCP client (Claude, any agent)
        │  tools
        ▼
  odds-mcp server ──── reads ────>  store.py   (consensus / fair price / outliers)
        │                              ▲
        └──── the one write ────> gate.py      propose → approve → publish
                                      │        (envelope · token · audit)
                                      └──────> out/audit_log.jsonl
```

## Tools

| Tool | Kind | Behavior |
|------|------|----------|
| `list_events` | read | events in the snapshot |
| `get_consensus` | read | books per market, price ranges, median fair odds, outliers |
| `get_fair_price` | read | one market's fair price — or an **explicit not-found with the valid alternatives**. Ambiguous line? It asks. Single book? "A single book is not a consensus." Never a guess. |
| `find_outliers` | read | books off the consensus median, with exact deviation % |
| `get_pipeline_health` | read | counts **plus every upstream devig refusal** — what the snapshot does *not* cover |
| `propose_boost` | write-intent | checked against the risk envelope (max % over fair, stake cap) *before* a human sees it; refusals carry the reason |
| `approve_proposal` | human gate | issues a **single-use** approval token (production: a trader's Telegram approval card; same gate logic) |
| `publish_boost` | write | requires proposal id + matching token; token dies on use; replay fails |
| `get_audit_trail` | read | the append-only log of every attempt and outcome |

## Run it

```bash
pip install -r requirements.txt
python -m pytest tests/ -q          # 13 tests
PYTHONPATH=src python -m odds_mcp.server    # stdio MCP server
```

Claude Desktop config:

```json
{"mcpServers": {"odds": {"command": "python", "args": ["-m", "odds_mcp.server"],
 "cwd": "<this repo>", "env": {"PYTHONPATH": "src"}}}}
```

## What the tests pin down

- Publishing **without** approval: refused. Wrong token: refused.
  Replaying a used token: refused. (Tokens are single-use and compared
  constant-time.)
- A boost priced above the envelope (default: 5% over consensus fair)
  is refused **at propose time**, with the arithmetic in the reason.
- `get_fair_price` on a market with two live lines demands an explicit
  `line` instead of picking one; on a single-book market it refuses to
  call one price a "consensus".
- Every attempt - including every refusal - lands in the audit log.
- All 9 tools are actually registered on the server (test asserts the
  exact set).

## Why this shape

An agent that can only read is safe but useless; an agent that can
write is useful but dangerous. The resolution is not "trust the model" -
it is structure: **envelope, then human, then token, then audit.** The
model can draft; only the gate can act; and the log never forgets.

MIT licensed. Part of a set of small, honest data gates:
[jigonyoo.com](https://jigonyoo.com).