Every key failed in exactly the same way

DHSeaDev — Chrome Extensions, Windows Tools, & Idle Games

I spent Monday building a read-only Model Context Protocol server over ServeManager‘s REST API — the job-management system the process-serving firm I work for runs on. The goal was modest: let an assistant read jobs, companies and court cases without me pasting exports into a chat window.

Every request came back HTTP 403 with a body five words long: error code: 1010. I lost an hour to the credential. The credential was fine.

The short version, for anyone who arrived here from that error string: Cloudflare error 1010 is not an authentication failure. It is a browser-signature ban issued at Cloudflare’s edge — “the owner of this website has banned your access based on your browser’s signature” — and Python’s default User-Agent is enough to trigger it. The request never reaches the API’s own auth layer, which is why every credential you try fails in exactly the same way. The fix is four request headers, not a new key.

Why does a correct API key still return 403?

ServeManager’s API uses the customer’s own API key over HTTP Basic. So I did the obvious loop: try the key as the username with an empty password, try it as a Bearer token, try it as an X-API-Key header, re-check the base64 padding, regenerate the key, try again. Six variants. Every one returned 403 and error code: 1010.

That is the shape of a permissions problem, and it is where you start writing an email to support asking what scope your key is missing. It is also where an hour goes.

How do you tell a Cloudflare block from an auth failure?

By whether the failures differ. An authentication layer discriminates — that is the entire job description. A malformed header should not fail the same way as a well-formed header carrying a revoked key, which should not fail the same way as a valid key hitting an endpoint it cannot see.

When six materially different requests produce byte-identical responses, nothing is reading those bytes. The answer is coming from something standing in front of the thing you are trying to talk to. That single observation is the whole diagnostic, and it generalises past Cloudflare: identical failure output across varied input means the input is not being examined.

In this case the thing in front was Cloudflare’s browser-integrity check. Python’s standard library announces itself as User-Agent: Python-urllib/3.x, and the User-Agent header is the cheapest fingerprint an edge can filter on. That string alone was enough to get every request I sent discarded before ServeManager’s servers ever saw it.

What actually fixes Cloudflare error 1010 in Python?

Four request headers: a browser User-Agent, plus Accept, Accept-Language and Accept-Encoding. No proxy, no scraping framework, no third-party client.

I proved it side by side with a key I made up on purpose. Default headers returned 403 and 1010. Browser headers returned 401 HTTP Basic: Access denied. — a correct rejection of a fake credential, which meant the request had finally reached the application. A 401 was the win condition.

The honest caveat: this defeats User-Agent fingerprinting and nothing else. If Cloudflare escalates to TLS or JA3 fingerprinting on that host, urllib stops working again and the answer becomes a browser-impersonating client. I have not tested that, because it has not happened.

How do you confirm an API endpoint exists without a key?

Call it with no credential and read which refusal you get. The useful consequence of the header fix showed up later, when I needed to know whether seven endpoint paths I had written tools against actually existed. Five of them I had never successfully called.

So I called all seven with the correct headers and no credential at all: account, jobs, companies, court cases, courts, employees, invoices. Every one returned 401. Not one returned 404.

Those two codes answer different questions. RFC 9110 §15.5.5 defines 404 as the origin server finding no current representation for the target resource — the route does not exist. §15.5.2 defines 401 as a request lacking valid authentication credentials — the route exists and is refusing you. That distinction confirmed all seven paths and re-confirmed the header workaround, using zero credentials and touching zero customer data.

It does not confirm everything. The field mappings behind five of those tools are still inferred rather than captured, and they are labelled that way in the README until someone runs them against a live key. Proving a door exists is not the same as knowing what is behind it.

Why did FastMCP stop existing?

Because the SDK moved. The mcp package on PyPI is now at 2.0.0, and FastMCP is gone from it. If your server dies on import with a ModuleNotFoundError pointing at mcp.server.fastmcp, that is why.

The replacement import is from mcp.server.mcpserver import MCPServer. The .tool() decorator and .run(transport="stdio") are unchanged, so the migration is one line — but every tutorial I could find, and about half of my own notes, still said mcp.server.fastmcp. Checked against the SDK, not against the tutorials, as of 4 August 2026.

What I took from it

  • Identical failures are a signal, not noise. If varying the input does not vary the output, you are not talking to the thing you think you are talking to.
  • A written environment fact is still a hypothesis. My own handoff notes said neither sandbox could reach that host at all. That was wrong — the network route was always fine, and the original blocker had only ever been a missing key. I nearly designed around a constraint that did not exist. The same lesson, in a different costume, as the four Qt bugs only pixels found.
  • Ask the question that costs nothing first. An unauthenticated probe distinguishes “wrong URL” from “no permission” before you have a credential, before you have written a client, and without touching anybody’s data.

The server shipped read-only: ten tools, 28 unit tests, no mutating operations. Anything that writes back into a system of record stays behind a confirmation gate, and that is a different post. If you want the rest of how I work on this kind of thing, there is a longer piece on the method, a companion note on the orchestrator layer that routes it, and — for the same firm’s data seen from the other end — ServeBoard, where deciding what counts as one job turned out to be the hard part.