MCP security and authentication for data access

By Arshad Ansari

MCP security starts with one question: how does the server know who is calling? For a local server the answer is easy, because it is your own process. For a server on a network, the 2026-07-28 MCP specification gives a full answer, built on OAuth 2.1. Most data-access incidents happen in the gap between the two cases.

This post covers MCP authentication end to end. It explains when it applies, how the OAuth flow works, the rules that matter most for servers that touch data, and what two production servers sent back when I called them with no credentials. Then it covers what I run in my own system, where the client is an agent I don't fully trust, and where OAuth alone was not enough.

stdio or HTTP: which rules apply

The spec makes authorization optional, then splits by transport:

  • stdio servers SHOULD NOT use the MCP authorization flow. They take credentials from the environment. The client launches the server as a child process, so the trust boundary is your machine and your user account.
  • HTTP servers SHOULD follow the flow when they support authorization, and once they do, most of it is MUST.

For a local server, then, MCP authentication means: which credentials did you put in its environment, and what can they do? A stdio server holding a read-write database URL is a read-write tool, whatever its README says. Give it a read-only role. The Postgres MCP server post shows the role.

The rest of this post is about the HTTP case.

How MCP OAuth works

The MCP server acts as an OAuth 2.1 resource server. A separate authorization server issues tokens; it may be your identity provider. The flow:

  1. The client calls the server with no token. The server replies 401 with a WWW-Authenticate header that points to its Protected Resource Metadata (RFC 9728). Servers MUST publish this.
  2. The client fetches that document, which names the authorization server, then fetches the authorization server's own metadata.
  3. The client identifies itself: pre-registered, via a Client ID Metadata Document, or via Dynamic Client Registration (more below).
  4. The client runs the authorization code flow with PKCE. It sends a resource parameter (RFC 8707) naming the MCP server, in both the authorization and the token request. Clients MUST send it, even if the authorization server ignores it.
  5. The client calls the MCP server with Authorization: Bearer <token> on every request. Tokens MUST NOT go in the query string.

The spec also requires clients to check the iss value in the authorization response against the issuer they expected (RFC 9207), which blocks mix-up attacks.

What two real servers sent back

I called two hosted data MCP servers with no credentials on 1 October 2026, to see the flow from the outside.

MotherDuck (https://api.motherduck.com/mcp): initialize returned a 401 with:

www-authenticate: Bearer resource_metadata="https://api.motherduck.com/.well-known/oauth-protected-resource/mcp", resource="https://api.motherduck.com/mcp"

The metadata document named https://mcp-auth.motherduck.com as the authorization server, with one scope, mcp:connect, and bearer tokens in the header only. The authorization server's metadata listed PKCE with S256, public clients (token_endpoint_auth_methods_supported: ["none"]), and a registration_endpoint for Dynamic Client Registration. It did not advertise Client ID Metadata Document support.

BigQuery (https://bigquery.googleapis.com/mcp): initialize and tools/list both answered without a token. The list had nine tools, from list_dataset_ids to execute_sql. Calling a tool returned 401, with a resource_metadata pointer whose document names https://accounts.google.com/ and the BigQuery OAuth scope.

Both follow the spec. They draw the public line in different places. BigQuery shows its tool list to anyone. MotherDuck shows nothing without a token. If your tool names or descriptions say something about your data, choose MotherDuck's line.

The token rules that matter for data

Three rules in the spec do most of the work for a server that reads data.

Audience. Servers MUST check that a token was issued for them, per RFC 8707. A token minted for your CRM's MCP server must not work on your warehouse's MCP server, even if the same identity provider issued both.

No token passthrough. The spec is blunt: servers "MUST only accept tokens that are valid for use with their own resources" and "MUST NOT accept or transit any other tokens". Two things are banned here. A server must not accept a token meant for another API. And it must not take the client's token and forward it downstream. If your MCP server calls a database API, it gets its own credential for that API. Otherwise the downstream API cannot tell the MCP server from the user. Your audit trail then records the wrong caller, and a stolen token works in places it was never meant for.

401 versus 403. Invalid or expired tokens get 401. A valid token without enough scope gets 403 with error="insufficient_scope" and the scopes needed, and the client can step up. Read-only and write tools can then live behind different scopes. A reader's token never carries write.

CIMD and DCR: how clients register

A client needs a client ID before it can start OAuth. The spec gives three ways, and the client registration page ranks them:

  1. Pre-registration. Use it if the client has credentials for this authorization server already.
  2. Client ID Metadata Documents (CIMD), if the authorization server advertises client_id_metadata_document_supported. The client ID is an HTTPS URL. The authorization server fetches a JSON document from it, checks that the redirect URI is listed there, and shows the client's name on the consent screen.
  3. Dynamic Client Registration (DCR), if the server has a registration_endpoint. The spec now marks it deprecated, kept for authorization servers that don't support CIMD yet.

Why the change matters for security: with DCR, any client can register itself and get a fresh ID, so the ID tells the server nothing. With CIMD, the ID is a URL on a domain the client controls. A server can decide to trust certain domains, and a client name can't be made up without controlling that domain.

As the MotherDuck probe shows, servers in production still run DCR. A client should support both for now.

What I run: scoped tokens and a server-side gate

My own system, AEGIS, serves its tools over MCP to agent runs it launches: headless Claude Code and Kimi CLI. It does not use the OAuth flow, because the clients aren't third parties. AEGIS creates every run itself. The threat is different too: the client is an agent reading untrusted content, which might be talked into misusing whatever it can reach. Two decisions came out of that, both in api/routes/mcp_server.py and services/mcp_tokens.py.

A mount token per run, bound to one endpoint. A run doesn't get the API key. It gets a token signed with HMAC-SHA256, naming one agent and one mode (gated or not), with an expiry: 6 hours by default, never less than 60 seconds. The server checks the token against the URL. Using it on another agent's endpoint, or on the ungated URL with a gated run's token, returns 403. The mode is part of the signed payload, so a run cannot downgrade itself. The admin API never accepts a mount token. The operator endpoint, which can start and stop runs, refuses mount tokens explicitly. The reason, from the code: an ungated run has a shell and can read its own config file. So the credential in that file must open nothing but its own door.

This is the spec's audience rule, in a home-made form: a token that works in exactly one place, and expires.

Approval enforced by the server, not the client. I first relied on the CLI's own permission prompt. A live test on 13 August 2026 showed Claude CLI 2.1.231 calling an AEGIS write tool in a gated run with no approval card at all: in headless -p mode, tools from an explicitly passed --mcp-config were trusted. So the gate moved into the server. On the gated endpoint, any tool not on a 29-name read-only list needs a person to approve it, whatever the client thinks.

The timing had to fit the client. The CLI abandons a tool call after about 60 seconds, but a person might take minutes. So the gate works as approve-then-retry. The first call raises a card in Slack or the admin panel. The server holds each attempt open for 40 seconds and then tells the model to retry with identical arguments. Ten retries give about seven minutes. The approval covers one execution of exactly the arguments on the card; change one argument and it needs a new card. Cards expire after 15 minutes. Anything other than an explicit approve, including an error inside the gate, is a deny.

The lesson carries over to any MCP deployment that touches data: the client's permission model is advice. The server's is the control.

A checklist for MCP authentication

For a server that reads data, before anyone else connects to it:

  1. stdio: credentials from the environment, scoped to read-only, never committed in .mcp.json.
  2. HTTP: a 401 with resource_metadata, a published Protected Resource Metadata document, and tokens accepted only in the Authorization header.
  3. Audience-checked tokens, and no passthrough. Downstream calls use the server's own credential.
  4. Separate scopes for read and write, with 403 insufficient_scope for step-up.
  5. CIMD supported on your authorization server, with DCR only as a fallback.
  6. Agent credentials bound to one server and one mode, with an expiry measured in hours.
  7. Writes approved on the server, failing closed.

Authentication decides who is calling. What they can do once inside (read-only roles, views, row caps, timeouts, audit) is the other half. That is in how to connect an LLM to your database safely. For a server where the model gets governed metrics instead of tables, see the dbt MCP server.

Local-First Analytics doesn't cover MCP or OAuth. It covers the data these servers expose, DuckDB over Parquet files (Chapters 3 to 5), and running a model against it locally so nothing leaves your machine (Chapter 12). Chapter 1 is free to read; the rest is on Amazon.

If you are opening an MCP server to agents or to other teams and want the auth design checked first, that is work I do. If your server only runs over stdio on your own laptop, item 1 is the whole job.

More in this series: DuckDB MCP server, building an MCP server with FastMCP and Postgres MCP server.

Common questions

Does MCP have authentication?
For servers reached over HTTP, yes: the MCP specification defines an optional authorization flow based on OAuth 2.1. When a server supports it, it acts as an OAuth resource server, publishes Protected Resource Metadata (RFC 9728), and expects a bearer token issued for it. For local servers that run over stdio, the spec says they should not use this flow and should take credentials from the environment instead.
How does MCP OAuth work?
The client calls the server without a token and gets a 401 whose WWW-Authenticate header points to the server's Protected Resource Metadata. That document names the authorization server. The client reads the authorization server's metadata, registers or identifies itself, and runs an authorization code flow with PKCE, sending a resource parameter (RFC 8707) that names the MCP server. The token it gets is valid only for that server, and travels in an Authorization: Bearer header on every request.
What is token passthrough in MCP and why is it banned?
Token passthrough is when an MCP server accepts a token that was not issued for it, or forwards the client's token to another API. The spec forbids both: servers must only accept tokens issued for their own resource, and must not accept or transit any other tokens. Passthrough breaks audience checks, rate limits and audit trails, and turns the server into a way to use a stolen token somewhere it was never meant to work. A server that calls another API should get its own token for that API.
What are MCP security best practices for data access?
Use stdio with environment credentials for local servers, and keep those credentials least-privileged. For remote servers, implement the spec's OAuth flow: Protected Resource Metadata, audience-checked tokens, no passthrough, tokens only in the Authorization header. Bind any credential you hand to an agent to one server and one mode, and give it an expiry. Enforce write approval on the server, not in the client, because the client's permission prompts can be bypassed.
What is CIMD in MCP, and is dynamic client registration deprecated?
CIMD, Client ID Metadata Documents, lets a client use an HTTPS URL as its client ID; the authorization server fetches a JSON document from that URL to learn the client's name and redirect URIs. The 2026-07-28 MCP spec says clients and authorization servers should support it. Dynamic Client Registration (RFC 7591) is now deprecated and kept only for backwards compatibility with authorization servers that do not support CIMD.

Get new posts by email

Data engineering notes like this one — what breaks and what it costs, in production.

What breaks and what it costs — pipelines, warehouse bills, and the failures that only show up in production. A few a month, never padded to hit a schedule. No sequence, no pitch deck. Reply 'stop' once and you're off — it reaches me, not a queue.

Want the whole playbook?

If this was useful, the long version is my book. Local-First Analytics — 313 pages, runnable code for every chapter — is the full build: DuckDB, Parquet and Arrow, from install to production. On Amazon, and chapter 1 is free to read here.

Get the book

Not ready to buy? Read chapter 1 free — the whole chapter, no email required.

Rather talk it through? Book a free 30-minute call. No slot that suits your time zone? Email info@hikmahtech.in.