Capability URLs vs OAuth for a personal MCP server
I needed a Model Context Protocol server that let a chat client read and write my own object storage. One user — me. The correct-sounding answer is OAuth. The answer I shipped was a capability URL, and I want to lay out that tradeoff honestly rather than pretend it was free.
What a capability URL is
Authority lives in the URL itself. The endpoint isn't at /mcp, it's at:
https://your-worker.example.workers.dev/<48-hex-random>/mcp
Anyone holding that string can call the server. Anyone who doesn't gets a bare 404 — not a 401,
not a 403. The service does not admit it exists.
That last detail matters more than it looks. A 401 tells a scanner "there is something here worth
attacking." A 404 tells it nothing, so the endpoint never enters anyone's target list.
The honest cost
Possession is access. There is no user, no session, no revocation-per-client, no consent screen, no scope negotiation. If the URL leaks — a screenshot, a pasted log, a synced clipboard, a chat transcript — whoever has it is you, until you rotate it.
Anyone claiming this is "just as secure as OAuth" is selling something. It isn't. It's a bearer token with worse ergonomics around rotation and no notion of identity.
Why it was still the right call here
OAuth's value is almost entirely in things a single-user server doesn't have:
| OAuth gives you | Do I have that problem? |
|---|---|
| Multiple users with different rights | No — one user |
| Per-client consent and revocation | No — one client |
| Scope negotiation | No — fixed toolset |
| Identity in the audit log | No — every call is me |
| Token refresh and expiry | Marginal — I can rotate a path |
What OAuth costs is an authorization server, a callback flow, token storage, refresh handling, and a client that implements the same. For a personal server that is weeks of work protecting a threat model that doesn't exist.
What actually makes it defensible
The capability URL isn't the whole design. Two other decisions carry most of the security weight, and they'd matter under OAuth too:
1. Contain the blast radius in the server, not the credential. Writes are forced under a single prefix. Deletes outside that prefix are refused outright. So a leaked URL can read, and can damage only what the agent itself created — it cannot destroy pre-existing content. This is the part people skip, and it's the part that means a leak is embarrassing rather than catastrophic.
2. Hold no credentials inside the server at all. Running on a platform with native storage bindings, the server reaches the bucket over internal infrastructure. There are no access keys in the worker, no secrets in environment variables, nothing to exfiltrate even with full code execution. The credential you didn't create can't leak.
Verify both by test rather than by intention. I tried to delete outside the allowed prefix and confirmed it was refused; "I designed it to refuse" is not the same claim.
When to stop doing this
Move to OAuth the moment any of these becomes true:
- A second human needs access
- You need to answer "who did this?" from a log
- Different clients should have different powers
- The data is somebody else's
Until then, a capability URL plus server-side containment plus zero stored credentials is a defensible design for a single-user tool — and, more to the point, one that actually gets built.
The general principle
Security decisions are only meaningful against a stated threat model. "Use OAuth" is good advice for a product and can be a shipping-blocker for a tool with one user. Write down who you're defending against before you pick the mechanism, and be honest in public about what you gave up.