---
title: "MCP Server Authentication: API Keys vs OAuth | Perceptlayer"
description: "The MCP spec makes OAuth 2.1 optional for remote servers. When a static API key in an Authorization header fits better, and how clients send one."
url: https://perceptlayer.com/blog/mcp-server-api-key-vs-oauth
---

[Blog](https://perceptlayer.com/blog) / [AI agents](https://perceptlayer.com/blog/category/ai-agents)

# MCP Server Authentication: API Keys vs OAuth

16 Oct 2026 Perceptlayer 8 min read

MCP server authentication comes in two shapes. The MCP specification defines an OAuth 2.1 flow for remote servers, but it makes authorization optional and lets clients and servers agree on their own scheme. For a server that acts for one account, a static API key sent as `Authorization: Bearer sk_live_...` is the simpler fit. OAuth earns its complexity when a server acts on behalf of many users who each sign in.

## Key takeaways

-   The MCP spec marks authorization OPTIONAL; HTTP-based servers that support it SHOULD follow its OAuth 2.1 profile, and STDIO servers SHOULD read credentials from the environment instead.
-   A server that follows the spec must publish OAuth 2.0 Protected Resource Metadata (RFC 9728) and point clients to it from a `401` response's `WWW-Authenticate` header.
-   A static key in a request header suits a server where the caller already has an account and a key: no consent screen, no token refresh, one value to rotate.
-   Perceptlayer's server at `https://api.perceptlayer.com/mcp` reads `Authorization: Bearer` or `X-API-Key`; `search_endpoints` and `get_endpoint` work without a key, and `call_endpoint` needs one.
-   Whichever you choose, never forward the client's token to another service: the spec forbids token passthrough.

## What does the MCP specification require for authentication?

Less than many guides imply. The specification says authorization is OPTIONAL for MCP implementations. When it's supported, implementations using an HTTP-based transport SHOULD conform to the spec's authorization section, and implementations using STDIO SHOULD NOT, retrieving credentials from the environment instead. Clients and servers MAY also negotiate their own custom authentication and authorization strategies ( [MCP specification, Authorization](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization), 2025).

If a server does follow the spec's OAuth profile, the requirements are firm. The server is an OAuth 2.1 resource server. It MUST implement OAuth 2.0 Protected Resource Metadata (RFC 9728), which tells clients which authorization server issues its tokens, and it signals that metadata through the `WWW-Authenticate` header on a `401` response. Clients MUST send the RFC 8707 `resource` parameter so tokens are bound to that server, and the server MUST reject tokens not issued for it ( [MCP specification, 2026-07-28](https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization), 2026).

How clients register with the authorization server has shifted between revisions. The 2025-11-25 revision made Client ID Metadata Documents the recommended route and moved Dynamic Client Registration from SHOULD to MAY, per the revision's own compatibility tables as summarised by Keycloak ( [Keycloak, Integrating with MCP](https://www.keycloak.org/securing-apps/mcp-authz-server), 2026). If you build the OAuth route, read the revision your clients speak, not a blog post from last year.

## When is a static API key the better fit?

When the server acts for an account the caller already has, and the person configuring the client is the person paying for the calls. That's most data and developer-tool APIs. The user creates a key in a dashboard, pastes it into the client's config once, and every request carries it. There's no consent screen to build, no tokens to refresh and no authorization server to run.

OAuth is the better fit when the server acts on behalf of users who shouldn't hand over a long-lived secret: a server that reads someone's mailbox, calendar or CRM. There, the user signs in through the provider, approves scopes, and the client holds a short-lived token it can lose without much damage. It's also the better fit when the client is a hosted app that many users connect, because each user needs their own grant.

| Question | Static API key | OAuth 2.1 (spec profile) |
| --- | --- | --- |
| Who is the caller? | The account owner, or their own agent | Many users, each signing in |
| Setup in the client | Paste a header value once | Sign-in flow, consent screen |
| What the server must run | Key lookup | Protected Resource Metadata, an authorization server, token validation |
| Credential lifetime | Until rotated or revoked | Short-lived tokens, refreshed |
| Scope | Everything the account can do | Scopes the user approved |
| Where it breaks down | Sharing one key across many people | More moving parts per client |

The weak spot of a key is that it's long-lived and broad. Treat it like a password: keep it out of committed config files, give each machine or agent its own key so you can revoke one without the others, and rotate it if it leaks. The [authentication docs](https://docs.perceptlayer.com/authentication) cover creating, rotating and revoking keys.

## How does Perceptlayer's MCP server read the key?

It reads the key from the request headers on every call. The server at `https://api.perceptlayer.com/mcp` speaks Streamable HTTP and is stateless: each POST is one JSON-RPC message. It takes the key from `Authorization: Bearer <key>` and falls back to `X-API-Key`. Keys start with `sk_live_`. There's no OAuth flow and no sign-in screen.

Not every tool needs the key. `search_endpoints` and `get_endpoint` only look up the catalogue, so they're public, like the API reference, and cost nothing. `call_endpoint` runs an endpoint, so it needs the key, and it goes through the same path as a direct API call: the same key check, parameter validation, rate limits and credits. A successful call spends the endpoint's credits, 1 for `/v1/google-search/search`, and a failed one spends nothing.

Two details follow from that design. The server takes the key only from headers: if a model puts an `api_key` field in a tool call's parameters, the server drops it, so a key never travels in a prompt or a URL. And because catalogue lookups are public, a missing key doesn't produce an HTTP `401` with a `WWW-Authenticate` challenge; `tools/list` succeeds, and `call_endpoint` returns a tool error instead.

## What happens when a call has no key? A worked example

Here's the path of one request from a client that was configured without a header. We haven't run these requests against the live server for this article; the behaviour below is read from the server's code. First, the client lists tools. That works, because listing is public. The model then picks `call_endpoint` and the client sends a JSON-RPC request like this one:

```json
{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tools/call",
  "params": {
    "name": "call_endpoint",
    "arguments": {
      "endpoint": "google-search.search",
      "params": { "query": "mcp authorization spec" }
    }
  }
}
```

The server finds no `Authorization` or `X-API-Key` header and runs the call anyway, so the key check fails the same way a direct API call would. The result comes back as a tool error, not an HTTP error. Its text is `Error invalid_api_key:` followed by the API's message, "Missing, unknown or revoked API key.", and, because no key was sent at all, a hint: "No API key was sent: add 'Authorization: Bearer ' to this MCP server's configuration." It ends with the request ID. No credits are spent.

That hint is meant for the model as much as the person. An agent that reads it can tell the user exactly what to fix, instead of retrying. With a valid key in the header, the same request returns the endpoint's JSON, followed by `Credits used: 1.` and `Credits remaining:` with the balance after the call.

The same applies to the other errors. A wrong parameter returns `invalid_parameters`, an exhausted balance `insufficient_credits`, a rate limit `rate_limited` with the seconds to wait. Each is a tool error with a code the model can act on; the [errors docs](https://docs.perceptlayer.com/errors) list them all.

## How do clients send the key?

Each client has its own place for a header. The setups below come from the [AI agents docs](https://docs.perceptlayer.com/ai-agents). Claude Code takes it on the command line:

```bash
claude mcp add --transport http perceptlayer https://api.perceptlayer.com/mcp \
  --header "Authorization: Bearer $API_KEY"
```

Cursor and VS Code take it in their `mcp.json` files. VS Code can prompt for the key once and store it, which keeps it out of the file:

```json
{
  "inputs": [
    { "type": "promptString", "id": "perceptlayer-key", "description": "Perceptlayer API key", "password": true }
  ],
  "servers": {
    "perceptlayer": {
      "type": "http",
      "url": "https://api.perceptlayer.com/mcp",
      "headers": { "Authorization": "Bearer ${input:perceptlayer-key}" }
    }
  }
}
```

Claude Desktop adds remote servers as custom connectors, where request headers are a beta not every account has yet; our guide to [adding an MCP server to Claude Desktop and Cursor](https://perceptlayer.com/blog/add-mcp-web-data-tools-claude-desktop) covers that and a config-file fallback. In your own code, SDKs that connect to Streamable HTTP servers accept headers on the transport.

## What should a key-based server still get right?

The rules that protect OAuth servers mostly apply to key-based ones too. The spec says an MCP server MUST NOT pass through the token it received from the client to another API, because the downstream service would trust a credential it never validated, and audit logs and rate limits stop meaning anything ( [MCP Security Best Practices](https://modelcontextprotocol.io/docs/2026-07-28/tutorials/security/security_best_practices), 2026). A key-based server should resolve the key to an account and call its own backends with its own credentials.

Beyond that: accept keys only over HTTPS, never in query strings a client might log, and answer a bad key with an error the model can read. Meter and rate-limit per account, not per key, so creating more keys doesn't buy more capacity; our post on [MCP server rate limiting](https://perceptlayer.com/blog/mcp-server-rate-limiting-agents) goes into the limits. And keep the expensive tools behind the key while discovery stays open, so an agent can learn what's available before anyone has configured anything.

To try the key-based setup, [get a free API key](https://perceptlayer.com/signup), add the server to your client with the header, and ask a question that needs a live search through the [Google Search API](https://perceptlayer.com/apis/google-search). Every verified account gets 1,000 free credits a month; [pricing](https://perceptlayer.com/pricing) has the packs.

## APIs in this post

-   [Google Search **1 credit per call** · 7 endpoints](https://perceptlayer.com/apis/google-search)

## Frequently asked questions

### Does the MCP spec require OAuth?

No. The specification makes authorization optional. If an HTTP-based server supports it, it should follow the spec's OAuth 2.1 profile, with Protected Resource Metadata (RFC 9728) and resource-bound tokens. STDIO servers should read credentials from the environment instead, and clients and servers may agree on their own scheme, such as an API key header.

### Is an API key in an MCP config secure enough?

For a server that acts for your own account, it can be, if you handle it like a password. Keep it out of committed files, use one key per machine or agent, send it only over HTTPS and rotate it if it leaks. For a server that acts on behalf of many users' private data, OAuth's short-lived, scoped tokens are the safer design.

### Which header does Perceptlayer's MCP server read?

`Authorization: Bearer sk_live_...`, with `X-API-Key` as a fallback. The server ignores an `api_key` sent inside a tool call's parameters, so the key never travels through the model. `search_endpoints` and `get_endpoint` work without a key; `call_endpoint` needs one and spends credits only on success.

### What does an MCP client see when the key is missing?

On Perceptlayer's server, listing tools still works. `call_endpoint` returns a tool error starting `Error invalid_api_key:`, with a hint to add an `Authorization: Bearer` header to the server's configuration and the request ID. No credits are spent, and the model can relay the fix to the user.

### Can a client switch between OAuth and API key servers?

Yes. A client that supports custom headers can talk to a key-based server, and one that implements the spec's OAuth flow can talk to an OAuth server. Many clients support both, but header support varies, and Claude Desktop's request headers are still a beta. Check your client's docs for where it takes headers.

mcp authentication oauth api keys ai agents

## More from the blog

1.  [AI agents](https://perceptlayer.com/blog/category/ai-agents) 12 Oct 2026 Perceptlayer 7 min read
    
    ## [MCP Server Too Many Tools: Keeping Agent Context Small](https://perceptlayer.com/blog/mcp-too-many-tools-context)
    
    One tool per endpoint for 1,659 endpoints is about 567,000 characters of schema. A 10-step checklist to keep an MCP server's tool list small.
    
2.  [AI agents](https://perceptlayer.com/blog/category/ai-agents) 2 Oct 2026 Perceptlayer 7 min read
    
    ## [How to Add an MCP Server to Claude Desktop and Cursor](https://perceptlayer.com/blog/add-mcp-web-data-tools-claude-desktop)
    
    Add the Perceptlayer MCP server to Claude Desktop or Cursor with an API key header, run a first Google search, and check only successful calls cost credits.
    
3.  [AI agents](https://perceptlayer.com/blog/category/ai-agents) 28 Sept 2026 Perceptlayer 7 min read
    
    ## [VS Code MCP Server Setup: Add Live Web Data to Copilot](https://perceptlayer.com/blog/vs-code-mcp-server-setup-web-data)
    
    Add Perceptlayer's MCP server to VS Code in .vscode/mcp.json with a password input for the key, run a first search in agent mode and check credits per call.
