Originally published on BotCookery, free build recipes for AI-built trading and prediction-market bots. Written with AI assistance. Every output line and count below is from the Oct 25, 2026 rebuild: each tool called through the MCP Python SDK's stdio client on live Polymarket US public data. No keys were involved, and the server has no trading tools. Registering it in an AI tool wasn't part of the test. Nothing here is financial advice. Market data only.

A read-only MCP server is a small local process that gives Claude Code, Cursor or any MCP client four tools (search markets, get market, best bid/offer, order book) backed by Polymarket US public endpoints. No API key, no order tools, so your assistant can look at markets but can't place anything.

On Oct 25, 2026 we pasted the Oct 22 version of the BotCookery prompt into a clean folder, built every file it asks for, following it line by line, and called each tool through the SDK's stdio client against live Polymarket US data that afternoon. The safety parts held: the server started, refused to run with POLYMARKET_SECRET_KEY set, and rejected bad slugs. The data parts didn't. search_markets("bitcoin", limit=20) returned 480 rows and about 135,000 characters, and get_market reported 99% on a market whose live book was bid 0.07, ask 0.99.

What the prompt builds

Four read-only tools on https://gateway.polymarket.us, no auth:

  • search_markets(query, limit) → GET /v1/search
  • get_market(slug) → GET /v1/market/slug/{slug}, plus best bid/offer
  • get_bbo(slug) → GET /v1/markets/{slug}/bbo
  • get_order_book(slug, depth) → GET /v1/markets/{slug}/book

Plus mock-HTTP tests, a smoke script that calls every tool over stdio, and a README with the register command for Claude Code and Cursor.

Bug 1: limit counted events, not markets

Polymarket's search limit counts events, and one event can hold dozens of markets. The literal build passed limit straight through. The old build at 4:40 PM ET (long rows trimmed):

--- search_markets {'query': 'bitcoin', 'limit': 20} chars=134823 [{"question":"Will Bitcoin be above ___ in 2026?", ...}, ... 480 rows] 

That's more than an assistant can usefully read, and 7 of those rows were resolved markets.

Bug 2: the price came from the wrong field

The old get_market read its price from outcomePrices, whose meaning changes between endpoints. On a Fed funds market it said "last 0.99, 99%" while the live book was bid 0.07, ask 0.99.

Bug 3: "implied %" from a book with nothing in the middle

The mid of a 0.11 / 0.89 book is 50%. That number tells you nothing, but the tool reported it anyway:

--- get_bbo aec-nfl-tb-chi-2026-11-08 {"yes_bid":0.11,"yes_ask":0.89,"yes_last":null,"implied_pct":50.0, ...} 

Bug 4: "YES" wasn't always yes

On game markets the long side is a team. "Who will win, Buccaneers vs Bears?" with a 0.11 bid doesn't say which team that price is for, and the old get_market for the same game returned "yes_last":0.89,"implied_pct":89.0 with no side named.

Bug 5: the register command couldn't import the SDK

The setup step said python3 /full/path/server.py. But mcp and httpx live in the project's venv, so system Python fails with ModuleNotFoundError. The SDK version wasn't pinned either, and many AI tools still write the v1 FastMCP import, which v2 renamed.

Bug 6: 429s far below the documented limit

We stayed far below the documented 25 requests a second, and Polymarket still answered 429 twice during testing.

What we changed

  • Search returns market rows, open only. Flatten events to one row per market (question, event, slug, long_side, status, end date), drop closed or inactive markets, then cut to limit rows. No prices in search; it says "use get_bbo for prices".
  • Prices only from /bbo and /book (bestBid, bestAsk, lastTradePx, any of which can be null). Never outcomePrices or currentPx.
  • Implied % only when the spread is 0.10 or less. Otherwise it's null with a reason, and a missing bid or ask is handled.
  • long_side named in every response, from the marketSides entry with long=true, with a note that every price is for that side.
  • Every response under 4,000 characters, dropping whole rows and keeping valid JSON with "truncated": true.
  • Slug regex ^[a-z0-9][a-z0-9-]*$, and on 429 a 1 s wait, exponential backoff, then a clear ToolError instead of hanging.
  • mcp[cli]>=2,<3 pinned in a venv, and the register command uses full paths to the venv's Python and server.py.

After: a fresh rebuild

The rebuilt server at about 4:45 PM ET, same calls (trimmed). 7 tests pass:

--- search_markets bitcoin, limit 20: 17 shown of 471 open matches, truncated, 3898 chars --- get_market aec-nfl-tb-chi-2026-11-08 {"long_side":"Buccaneers","yes_bid":0.11,"yes_ask":0.89,"yes_last":null, "spread":0.78,"implied_pct":null,"implied_reason":"spread 0.78 too wide", "note":"Prices are for the long side: Buccaneers.", ...} --- get_bbo aachc-nfl-2027-01-10-lastundefeated-kc {"yes_bid":0.27,"yes_ask":0.33,"yes_last":0.35,"spread":0.06, "implied_pct":30.0, ...} 

Search asked for 20 rows and stopped at 17 to stay under 4,000 characters. The wide book says why it has no implied %, and the tight one gets one.

Register it (full paths)

claude mcp add --transport stdio polymarket-readonly -- /full/path/.venv/bin/python /full/path/server.py 

Or in Cursor's .cursor/mcp.json:

{"mcpServers": {"polymarket-readonly": { "command": "/full/path/.venv/bin/python", "args": ["/full/path/server.py"]}}} 

Built this before Oct 25?

Paste this follow-up into the same project:

Update my read-only Polymarket US MCP server. Keep it read-only and keep the startup check that refuses POLYMARKET_SECRET_KEY. 1. search_markets: the API's limit counts EVENTS, not markets. Flatten to one row per market (question, event, slug, long_side, status, endDate), drop markets that aren't active or are closed, then cut to `limit` rows. No prices in search; say "use get_bbo for prices". 2. Prices: take YES prices ONLY from /bbo or /book (bestBid, bestAsk, lastTradePx; any can be null). Stop using outcomePrices and currentPx. implied_pct is the mid only when both sides exist and the spread is <= 0.10; otherwise null with a reason. 3. long_side: the description of the marketSides entry with long=true ("Yes", or a team name on game markets). Show it in search rows and get_market, and say every price is for that side. 4. Keep every response under 4,000 characters by dropping whole rows (valid JSON, "truncated": true). Keep the closing line. 5. Slugs must match ^[a-z0-9][a-z0-9-]*$. On 429 wait 1s, then back off exponentially; after a few tries raise a ToolError. 6. Pin "mcp[cli]>=2,<3" in a venv. Register with FULL paths to the venv's python and server.py, not plain python3. 7. Add mock tests for the row limit, closed markets, a null ask, a wide spread and long_side. Run all tests and the stdio smoke script. 

Starting fresh? The full one-shot prompt is on the recipe page.

Why read-only

An MCP tool can be called by the AI on its own initiative, and agents make mistakes or get steered by text they read, including market text the tools return. Leaving order tools out removes the worst failure. If you later want trading, build it as a separate program with a paper/live gate, and don't hand an agent an order tool. Also: an MCP server runs code on your machine with your permissions, so only install ones you've read or trust.

Limits

The page's verified line, word for word: Verified working on live data in read-only mode, Oct 25, 2026. Every tool called through the SDK's stdio client on live public data. Registering it in an AI tool wasn't tested.

So this run proves the tools return sane, bounded data over stdio on live Polymarket US markets. It doesn't prove how any particular assistant uses them once registered. The output is market data, not a signal.

The full recipe, with the complete prompt, the step-by-step build, the safety checklist and every doc we checked, is at botcookery.com/recipes/polymarket-mcp-server-read-only.

API changes and new field notes: @botcookery on X (https://x.com/botcookery)