Build an OddsPapi MCP Server: Plug 350+ Bookmakers Into Claude, Cursor & Cline (Python)

Build an MCP Server - OddsPapi API Blog
How To Guides May 21, 2026

Your AI assistant can’t see live odds. Ask Claude or Cursor “what’s the best price on Liverpool tonight?” and you’ll get a wall of caveats — it has no live data, no API access, no way to fetch a quote at runtime. The fix is an MCP server: a tiny Python process that exposes a few tools to any MCP-compatible client (Claude Desktop, Cursor, Cline, Continue) and lets the model call real APIs on your behalf.

This post walks you through a working OddsPapi MCP server in roughly 80 lines of Python. By the end, your agent can list sports, scan upcoming fixtures, and find the best price on a market across 350+ bookmakers — without you copy-pasting JSON into chat.

Why an MCP Server Beats Paste-and-Pray

Before MCP existed, getting live data into Claude or Cursor meant one of three workflows, all bad:

  1. Paste JSON into the chat. Works once. Stale 90 seconds later.
  2. Hand the model your API key and tell it to write requests in code. The code runs in a sandbox that often can’t reach the internet, or you’re shipping the key into a tool you don’t fully control.
  3. Scrape and mirror to a local file. Now you’re maintaining a sync layer instead of writing your strategy.

An MCP server flips this. You run a small process locally, it holds the API key, and the LLM client calls it through the Model Context Protocol — a stdio JSON-RPC handshake that every major AI dev tool now supports. The model never sees the key. You don’t paste anything. The data is live every call.

OddsPapi is unusually good as the backend for this:

  • 350+ bookmakers on a single endpoint — Pinnacle, Bet365, DraftKings, Polymarket, Betfair Exchange, and 357 more. The agent gets sharp and soft prices from one tool call.
  • Free tier with historical odds. Most odds APIs paywall historical data behind a $300/month plan. OddsPapi gives you backtest-quality price history on the free tier — you can build agents that justify their picks against last week’s data.
  • Clean JSON, no schema gymnastics. The response shape is consistent across sports. Agents handle it well without prompt engineering.
  • Query-string auth. No header gymnastics, no OAuth dance. Drop a key in an env var and you’re live.
Approach Latency Coverage Setup
Paste odds into chat Stale on arrival Whatever you pasted 30 seconds
Browser tool / web search 5–15s per query 1–2 books, blocked often Built-in
Custom function-calling code ~1s Whatever you wire Hours, per-tool retry logic
OddsPapi MCP server ~300ms 350+ books, 69 sports ~10 minutes

Step 1: Install the MCP SDK

Spin up a clean virtualenv. The MCP Python SDK pulls in pydantic and a tiny stdio runtime — nothing else.

python3 -m venv .venv
source .venv/bin/activate
pip install "mcp[cli]>=1.0" requests

Get a free OddsPapi key from oddspapi.io if you don’t have one. Keep it in an env var; never hard-code it.

export ODDSPAPI_API_KEY="your-key-here"

Smoke-test the API

Before wiring anything up to MCP, confirm the key works. The auth pattern for OddsPapi is a query parameter — not a header — which trips up devs migrating from other APIs.

import os, requests

API_KEY = os.environ["ODDSPAPI_API_KEY"]
r = requests.get(
    "https://api.oddspapi.io/v4/sports",
    params={"apiKey": API_KEY},
    timeout=10,
)
print(r.status_code, len(r.json()), "sports")
# 200 69 sports

69 sports, 367 bookmakers, every market type from 1X2 to Asian Handicap to player props. Now we wrap it.

Step 2: The MCP Server (80 Lines)

FastMCP — the high-level server class shipped with the SDK — turns each Python function into an MCP tool with a docstring-derived schema. Save the file as oddspapi_mcp_server.py:

"""OddsPapi MCP Server.

Exposes OddsPapi REST endpoints as MCP tools so any MCP client
(Claude Desktop, Cursor, Cline, Continue) can query live odds.
"""

import os
from datetime import datetime, timedelta, timezone

import requests
from mcp.server.fastmcp import FastMCP

API_KEY = os.environ.get("ODDSPAPI_API_KEY")
BASE_URL = "https://api.oddspapi.io/v4"

if not API_KEY:
    raise RuntimeError("Set ODDSPAPI_API_KEY in your MCP client config.")

mcp = FastMCP("oddspapi")


def _get(path: str, **params):
    params["apiKey"] = API_KEY
    r = requests.get(f"{BASE_URL}{path}", params=params, timeout=15)
    r.raise_for_status()
    return r.json()


@mcp.tool()
def list_sports() -> list[dict]:
    """Return every sport OddsPapi covers (id, slug, name)."""
    return [
        {"sportId": s["sportId"], "slug": s["slug"], "name": s["sportName"]}
        for s in _get("/sports")
    ]


@mcp.tool()
def list_bookmakers(sharps_only: bool = False) -> list[dict]:
    """Return all 350+ bookmakers. Set sharps_only=True for Pinnacle, Singbet, SBOBET."""
    sharp_slugs = {"pinnacle", "singbet", "sbobet"}
    rows = _get("/bookmakers")
    if sharps_only:
        rows = [b for b in rows if b["slug"] in sharp_slugs]
    return [{"slug": b["slug"], "name": b["bookmakerName"]} for b in rows]


@mcp.tool()
def upcoming_fixtures(sport_id: int, days: int = 2, limit: int = 25) -> list[dict]:
    """List upcoming pre-game fixtures with odds for a sport. Default: next 2 days."""
    today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
    end = (datetime.now(timezone.utc) + timedelta(days=days)).strftime("%Y-%m-%d")
    fixtures = _get("/fixtures", sportId=sport_id, **{"from": today, "to": end})
    out = []
    for f in fixtures:
        if not f.get("hasOdds") or f.get("statusName") != "Pre-Game":
            continue
        out.append({
            "fixtureId": f["fixtureId"],
            "home": f.get("participant1Name"),
            "away": f.get("participant2Name"),
            "tournament": f.get("tournamentName"),
            "country": f.get("categoryName"),
            "startTime": f.get("startTime"),
        })
        if len(out) >= limit:
            break
    return out


@mcp.tool()
def best_price(fixture_id: str, market_id: int = 101, outcome_id: int = 101) -> dict:
    """Best decimal price across all books for a given outcome. Default: 1X2 home win."""
    data = _get("/odds", fixtureId=fixture_id)
    bo = data.get("bookmakerOdds") or {}
    best_book, best = None, 0.0
    quotes = []
    for slug, body in bo.items():
        outcome = (
            body.get("markets", {})
            .get(str(market_id), {})
            .get("outcomes", {})
            .get(str(outcome_id), {})
        )
        price_obj = outcome.get("players", {}).get("0") or {}
        if not price_obj.get("active"):
            continue
        price = price_obj.get("price")
        if price and price > best:
            best, best_book = price, slug
        if price:
            quotes.append({"book": slug, "price": price})
    quotes.sort(key=lambda x: x["price"], reverse=True)
    return {
        "fixtureId": fixture_id,
        "marketId": market_id,
        "outcomeId": outcome_id,
        "bookCount": len(quotes),
        "best": {"book": best_book, "price": best},
        "topFive": quotes[:5],
    }


if __name__ == "__main__":
    mcp.run(transport="stdio")

That’s the entire server. Four tools, ~80 lines, works against the live API. A few notes on what’s happening:

  • The active guard in best_price is mandatory. OddsPapi ships outcomes that are listed but suspended (mid-game stoppage, low limits, market closed). Iterating naively will return ghost prices that don’t trade. Always filter on players["0"]["active"] == True.
  • Hardcoded market 101 = Full Time 1X2. Outcome 101 = Home, 102 = Draw, 103 = Away. For Asian Handicap, totals, BTTS, or player props, query /v4/markets?sportId=10 and let the agent build its own lookup. We unpack a richer market catalog in the FAQ below.
  • The _get helper centralises auth. If you spin up a Pro plan and switch to WebSocket, only this function changes.

Run it once standalone to make sure it boots

python oddspapi_mcp_server.py
# (no output — it's now reading JSON-RPC on stdin)
# Ctrl-C to exit.

If it raises RuntimeError: Set ODDSPAPI_API_KEY, the env var didn’t propagate. On Windows PowerShell use $env:ODDSPAPI_API_KEY = "..."; on macOS/Linux it’s export.

Step 3: Wire It Into Claude Desktop

Claude Desktop reads MCP servers from claude_desktop_config.json. Locations:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json

Add the OddsPapi entry inside mcpServers:

{
  "mcpServers": {
    "oddspapi": {
      "command": "/absolute/path/to/.venv/bin/python",
      "args": ["/absolute/path/to/oddspapi_mcp_server.py"],
      "env": {
        "ODDSPAPI_API_KEY": "your-key-here"
      }
    }
  }
}

Use absolute paths for both command and the script — Claude Desktop doesn’t inherit your shell PATH. Restart the app. You should see “oddspapi” listed in the MCP tools indicator at the bottom of the chat input. If it doesn’t appear, check Claude Desktop’s MCP log: ~/Library/Logs/Claude/mcp-server-oddspapi.log on macOS.

Cursor & Cline

Cursor uses the same JSON shape under ~/.cursor/mcp.json. Cline (VS Code extension) configures via the extension UI but stores the same config in ~/.config/cline/cline_mcp_settings.json. The OddsPapi entry above works unchanged in all three — that’s the whole point of MCP.

Step 4: Talk to Your Agent

Open Claude Desktop and ask something the model couldn’t possibly answer from training data:

“Use the oddspapi tools to find the best price on Liverpool to beat Manchester United today. Show me the top five books.”

Claude calls upcoming_fixtures(sport_id=10, days=1), finds the fixture, calls best_price(fixture_id="id1000001761301215", market_id=101, outcome_id=103), and returns:

{
  "fixtureId": "id1000001761301215",
  "marketId": 101,
  "outcomeId": 103,
  "bookCount": 106,
  "best": { "book": "hardrockbet", "price": 3.0 },
  "topFive": [
    { "book": "hardrockbet", "price": 3.0   },
    { "book": "kalshi",      "price": 2.941 },
    { "book": "polymarket",  "price": 2.941 },
    { "book": "betfair-ex",  "price": 2.94  },
    { "book": "1xbet",       "price": 2.904 }
  ]
}

That’s a real call against 106 active US-and-global books captured at kickoff: Hard Rock at 3.00 is +2.0% over the Pinnacle line of 2.85, with Kalshi and Polymarket effectively tied for second on the prediction-market side. If you saw that in a value-betting scanner you’d act on it — but instead of writing the scanner, you just asked.

Step 5: Stack More Tools (When You Outgrow the Basics)

The server above gets you running, but four tools is just the floor. The pattern scales — every additional @mcp.tool() is one more capability your agent gets without touching client code. Stuff that’s worth wiring next:

  • Historical odds — wrap /v4/historical-odds with a fixture_id, bookmakers signature (max 3 books per call). Now your agent can backtest a closing-line claim against last week’s prices. We dig into this in our historical odds guide.
  • Market catalog — wrap /v4/markets?sportId=X so the agent can discover Asian Handicap, BTTS, totals, or player-prop market IDs at runtime instead of hardcoding them. Soccer alone has 32,814 market-handicap-period combinations.
  • Arbitrage scan — copy the cross-book sum logic from our arb bot tutorial, drop it behind @mcp.tool(), and your agent can answer “any 2%+ arbs in the EPL today?” with a single tool call.
  • Steam-move detection — surface changedAt timestamps from the odds payload to flag Pinnacle line moves in real time. Same pattern as our steam detector but exposed as an agent tool.

Be deliberate about token cost. list_bookmakers() without a filter returns 367 rows — every call burns ~3,000 tokens of context. Add a search parameter or paginate before you wire it into a hot path.

Common Pitfalls

  • Server shows up in the client but tools fail silently. 90% of the time this is the env var. os.environ.get("ODDSPAPI_API_KEY") returns None because the MCP client didn’t pass it through. Pass it explicitly via the env block in the config (shown above) — don’t rely on inheritance.
  • Tools listed but Claude refuses to call them. Tool descriptions matter. The docstring is the model’s only signal about when to use the tool. “List sports” is fine; “List sports OddsPapi covers including soccer (10), basketball (11), tennis (13)” gives the agent enough context to pick correctly without an explicit sportId from the user.
  • Claude calls the tool but the JSON is huge. MCP responses get encoded into context. list_bookmakers = 367 rows ≈ 3,000 tokens. Use sharps_only or add a limit param. Same applies to fixtures — never return more than 25 unless asked.
  • Polymarket prices look wrong. They’re not. OddsPapi converts Polymarket’s native 0–1 share price to decimal odds for you, and ships the lay side under exchangeMeta. If you want order-book depth, that’s where it lives.

What This Unlocks

Once your agent has a tool layer for live odds, the workflow shifts. You stop writing one-off scripts to answer questions like “is the Liverpool line moving?” and start asking the agent. Things that work surprisingly well as natural-language queries:

  • “Pull tonight’s MLS card and flag any games where DraftKings is more than 3% off Pinnacle.”
  • “Compare the closing line on tomorrow’s Lakers game across Bovada, BetMGM, and Pinnacle. Which is sharpest?”
  • “List all Champions League fixtures this week with O/U 2.5 priced under 2.0 anywhere.”
  • “Build a Python script that scans 100 fixtures for arbitrages, using my arb-detection logic.”

The last one is the meta-move: the agent can call your odds tool while writing the script that uses your odds API. It tests its own output against live data before handing you code. That’s the unlock.

FAQ

Do I need a paid OddsPapi plan?

No. The free tier covers everything in this tutorial — 350+ bookmakers, 69 sports, free historical odds. Upgrade only if you need WebSockets (real-time push) or higher request quotas.

Does this work with Cursor and Cline, or just Claude Desktop?

Any MCP-compatible client works. The same JSON config block (with paths adjusted) drops into Cursor’s ~/.cursor/mcp.json, Cline’s settings file, and Continue’s config. The protocol is identical across clients.

Is my API key exposed to the model?

No. The key lives in the MCP server process as an environment variable. The client (Claude/Cursor) only sends tool calls — function name and arguments — over the stdio JSON-RPC channel. The model never sees the key.

What’s the latency like?

OddsPapi’s REST endpoints respond in 200–400ms typically. Add ~50ms for MCP serialisation. End-to-end, expect ~300ms per tool call from the model’s perspective. For sub-second push updates you need the WebSocket endpoint (paid plan).

Can I add more tools later?

Yes — every additional @mcp.tool() decorator adds a new capability. Restart the client to pick up changes. There’s no per-tool fee or registration; the SDK reflects the function signature and docstring into a schema automatically.

Why is my server appearing in the client but tools error out?

Almost always the API key isn’t being passed through. Add an explicit "env": {"ODDSPAPI_API_KEY": "..."} block in your MCP client config — don’t rely on shell-level inheritance, MCP clients spawn the server with a clean environment.

Stop Pasting JSON Into Chat

Once your agent can call live odds, you’ll wonder how you ever shipped without it. Get a free OddsPapi API key, drop in the 80 lines above, and your next “what’s the best price on…” question gets a real answer instead of a “I can’t access live data” stall.

If you’re building further: our free odds API guide covers the underlying endpoints in depth, the live betting API post explains how to scope in-play queries, and line shopping in Python shows the cross-book scanning logic that best_price is the simplest version of.