To let an AI assistant work with real business data over the Model Context Protocol (MCP), the server needs two things done properly: authentication (OAuth 2.1 with PKCE, so the AI client never sees a password) and authorization (so the assistant can see exactly what the signed-in person can see, and nothing more). For the Nitspot CRM connector I built, the most useful decision was to make the OAuth access token a real user session, which meant the database's row-level security did all the authorization.
What does an MCP connector for a CRM need to do?
Nitspot is a CRM for sales teams: contacts, a lead pipeline, prospects, deals, meetings and reports. The connector lets an AI client such as Claude answer questions like "which leads in the proposal stage have no next action this week?" straight from the data.
That raises three questions:
- Who is asking? The AI client acts on behalf of a person, so it needs that person's consent and a token, not a shared API key.
- What may they see? A sales rep (SDR) must never see deals, even through an AI assistant. An admin can.
- What may the AI do? Reading is useful. Writing on behalf of a model that can be prompt-injected is a much bigger decision.
How does OAuth 2.1 for MCP work?
MCP's authorization spec builds on OAuth 2.1. The flow I implemented:
- Discovery. A request without a token gets a
401with aWWW-Authenticateheader pointing to/.well-known/oauth-protected-resource, which names the authorization server. Its metadata (/.well-known/oauth-authorization-server) lists the endpoints and capabilities. - Dynamic client registration (RFC 7591). The AI client registers itself and its redirect URIs. These are public clients: there's no client secret, because a desktop app can't keep one.
- Authorization with PKCE. The client sends a
code_challenge(S256 only). The user signs in and sees a consent page in the app. - Token exchange. The client trades the code plus its
code_verifierfor an access token and a refresh token.
Which security details actually matter?
The spec leaves room for mistakes. These are the choices I'd make again:
- PKCE S256 is mandatory. There's no
plainmethod and no PKCE-less fallback. The verifier is checked against RFC 7636's format (43–128 unreserved characters) before hashing. - Redirect URIs are matched exactly against what was registered. They must be
https(plainhttponly for loopback addresses), with no fragment and no embedded credentials. An unregistered redirect target is never redirected to, even to report an error. - Codes are single-use, and claimed in one statement. The token endpoint marks a code used with a single conditional
UPDATE(exists, unused, unexpired). Two parallel exchanges can't both win. - A replayed code revokes what it produced. If a code that was already used shows up again, someone else has it, so the tokens it minted are revoked.
- Codes live 5 minutes and only their hashes are stored. The same goes for refresh tokens, which are also bound to the client that received them and rotated on every use.
- There's a kill switch. Every OAuth endpoint returns
404unless the owner explicitly enables it.
Why use the user's session as the access token?
This is the decision that removed the most code. When the code is exchanged, the server mints a real Supabase session for that user and returns it as the access token. Every MCP tool then runs queries with the caller's own token, so row-level security (RLS) in Postgres decides what comes back, exactly as it does for the web app.
There's no second permission system to keep in sync. An SDR who can't see deals in the app can't see them through Claude either, because the database refuses the query, not because some MCP-specific check remembered to.
Should AI tools be allowed to write data?
Not in the first version. The connector exposes nine read-only tools (search contacts, get a contact, list deals, pipeline summary, opportunity report and so on). Each carries a readOnlyHint annotation, and the server only implements initialize, ping, tools/list and tools/call. Anything else is "method not found".
Two smaller details made the tools much better for a model to use:
- Admin-only tools are hidden from non-admins in
tools/list. If one is called anyway, the error explains the reason in plain words ("deals are not visible to your role") so the model doesn't retry blindly. - Results are never truncated mid-JSON. If a result is too large, the tool returns a clear error telling the model to narrow the query with a smaller limit or more filters. Half a JSON object is worse than none.
What about browsers and DNS rebinding?
An MCP endpoint that accepts bearer tokens over HTTP can be reached by any web page the user visits. The server rejects requests whose Origin header isn't the app's own origin. Native MCP clients send no Origin, so they're unaffected.
The short version
- Use OAuth 2.1 with mandatory PKCE S256, exact redirect matching and single-use, short-lived, hashed codes.
- Reuse your existing authorization (here, RLS) by making the access token a real user session.
- Start read-only, hide what the user can't use, and write error messages for the model, not just for logs.
I've also written about a context-compression bug in an LLM gateway that made MCP agents loop. More of my AI engineering work is on the projects page.
Written by Ali Ahmed · Original article
