Capability URLs vs OAuth for a personal MCP server

2026-08-25 · Nikhil “Nick” Harinath · ~5 min

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.

Related notes

← All notes  ·  Work with me