DEV Community

John
John

Posted on Originally published at data.jiangzhang.ca

Building an MCP server for AI signals data — and the Cloudflare 403 that silently broke it

I wanted my coding agent to answer "what's happening in AI today?" without me pasting links into the chat. So I wrote a small MCP server for the AI signals data I collect. Two things came out of it that might save you time.

1. The MCP server (single file, stdlib only, no API key)

Five tools, all read-only:

Tool Returns
daily_signals today's signals — one per source (GitHub, Hugging Face, YouTube, Silicon Valley news), each linking to the original
what_changed deltas since the previous snapshot: new entries, entities no longer seen, rank movers
top_models Hugging Face models with rank, rank movement, momentum
top_repos GitHub repos, same shape
archive_days which daily editions exist, so the agent can cite a date

Every ranked row includes a citation string, so the agent cites a number instead of inventing one. Rows whose number can't be derived come back as unranked with the reason — I'd rather return "not ranked" than a plausible-looking guess.

Install is one file plus a config block:

{
  "mcpServers": {
    "signals": {
      "command": "python3",
      "args": ["/absolute/path/to/mcp_server.py"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Repo: https://github.com/jiangzhangcc-glitch/signals-api-mcp (MIT, Python 3.9+, no dependencies)

2. The trap: a 403 that your server never sees

My first version called the API with plain urllib.request.urlopen(url). Every tool failed with HTTP 403. The API was fine — the CDN in front of it rejects the bare Python-urllib/… User-Agent (and empty User-Agents) before the request ever reaches the origin.

The nasty part: because the request is blocked at the edge, the origin server has no log of it. If you only look at your own access logs, you conclude "nobody is calling my API". I measured it across clients:

User-Agent Result
Python-urllib/3.12 (stdlib default) 403
(empty) 403
python-requests/2.31.0 200
aiohttp, httpx, Go-http-client, node-fetch, axios, okhttp, Java 200
curl/8.5.0 200

So requests is fine — but the zero-dependency example that a reader will copy-paste is exactly the one that breaks. If you publish an API, test it with the dumbest client you can write, and set a real User-Agent in every sample:

curl -H 'User-Agent: my-app/1.0' https://data.jiangzhang.ca/signals4/api/today.json
Enter fullscreen mode Exit fullscreen mode

The server now sends SignalsAPI-MCP/1.0 (+https://…) on every request, and the 403 case returns an error message that names the likely cause instead of a bare status code.

The data behind it

I run a small daily pipeline that collects from public sources (GitHub trending, Hugging Face, YouTube, arXiv, arena boards, tech news, hardware/chips), dedupes and archives it. The newest daily delta is public, plus a machine-readable dataset:

The free tier is one edition a day from four core sources. There's a paid feed if you need eight sources, four updates a day, and the full history as an API — it's linked from the repo README. Everything in the MCP server stays free.

If you build something with it, I'd genuinely like to know what breaks — the 403 above only surfaced because I used my own server as a user.

Top comments (1)

Collapse
 
nikolas_dimitroulakis_d23 profile image
Nikolas Dimitroulakis •

ha, a 403 that never reaches your logs is the worst kind. Did you end up allowing Python-urllib at the CDN, or just tell people to set a User-Agent? I co-founded ApyHub, and from our experience at ApyHub a listing becomes callable through our MCP with nothing extra to build, so the paid feed could sit there per call too: around 10 minutes, no exclusivity, apyhub.com/become-a-provider