---
title: "MCP Server Returns a Login Page or 403 Instead of 401"
description: "Platform auth gates answer unauthenticated MCP clients with a browser login redirect instead of a 401, breaking the connection. The cause and fix on each platform."
canonicalUrl: "https://zuplo.com/learn/mcp/errors/302-redirect-instead-of-401"
pageType: "mcp-error"
errorString: "Your MCP server returns a login page or 403 instead of 401"
lastVerified: "2026-07-24"
specVersion: "2026-07-28"
---

# `Your MCP server returns a login page or 403 instead of 401`

> Something in front of your MCP server is enforcing authentication built for humans in browsers. Instead of the 401 an MCP client needs, it returns a login page, a redirect to one, or a bare 403 — so the client never learns how to authenticate.

_MCP Specification 2026-07-28._

## Is this you?

Call the endpoint with no credentials and read the raw status line and headers. This separates a platform gate from an application-level auth problem before any code changes.

```bash
curl -i -sS -X POST https://your-server.example.com/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

**This error** — A 302 or 307 with a `Location` pointing at a login page, a 200 whose body starts with `<!DOCTYPE html>`, or a 403 with no `WWW-Authenticate` header. Any of these means the gate answered, not your server.

**Working** — HTTP/1.1 401, a JSON body, and — if you are running the spec OAuth flow — a `WWW-Authenticate: Bearer` header naming your resource metadata URL. A successful 200 with a JSON-RPC result is also fine; it means auth is not the problem. If this is what you see, this is not your problem.

**Clients also report this as:**

- `MCP client gets HTML instead of JSON`
- `Unexpected token '<', "<!DOCTYPE "... is not valid JSON`
- `MCP server connection failed, received 403 Forbidden`

## Fix it

**In short.** A platform-level access gate is intercepting the request before your server sees it. Either disable that gate for the MCP route and handle authentication in your own code, or configure it to return 401 for unauthenticated API requests instead of redirecting. Every fix below is one of those two moves.

The symptom is identical across platforms; the cause is not.

### Vercel

If you deploy on Vercel and have Deployment Protection enabled

1. Vercel's own documentation states that automated systems receive a login page or a 403 rather than your deployment content. This applies to Vercel Authentication and Password Protection alike.
2. For a preview or internal deployment, send the bypass secret as a header on every request. Generate it in Project Settings under Deployment Protection.
3. For a production MCP server, turn Deployment Protection off for that project and authenticate in your own handler instead — the gate is not a substitute for application auth.

```bash
# Bypass header for a protected deployment
curl -X POST https://your-app.vercel.app/mcp \
  -H "x-vercel-protection-bypass: $VERCEL_AUTOMATION_BYPASS_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

[Vercel: automated agent access](https://vercel.com/docs/deployment-protection/automated-agent-access)

### Azure App Service

If you use Azure App Service or Functions built-in authentication (Easy Auth)

1. By default Easy Auth redirects unauthenticated requests to the identity provider's login page.
2. Set the unauthenticated action to return 401 instead of redirecting, so API clients get a status they can act on.
3. If your MCP server validates tokens itself, set the action to allow anonymous requests and let your handler enforce auth.

```bash
az webapp auth update \
  --name <app-name> \
  --resource-group <group> \
  --unauthenticated-client-action Return401
```

[Azure App Service authentication](https://learn.microsoft.com/en-us/azure/app-service/overview-authentication-authorization)

### AWS Load Balancer

If your MCP server sits behind an AWS Application Load Balancer with an OIDC action

1. The `authenticate-oidc` action performs a browser redirect flow, which an MCP client cannot complete.
2. Add a listener rule that matches your MCP path and forwards straight to the target group, ahead of the authenticate rule.
3. Validate the bearer token in the MCP server, or in front of it with something that speaks the MCP auth flow.

[ALB: authenticate users](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-authenticate-users.html)

### oauth2-proxy

If you run oauth2-proxy in front of the server on Kubernetes

1. oauth2-proxy is a browser cookie-session proxy. It is the reflexive answer for protecting an internal service and it is structurally wrong for MCP, because the client presents a bearer token and has no cookie jar.
2. Route the MCP path around oauth2-proxy, and validate the token in the server or at an MCP-aware gateway.
3. If you only need to accept JWTs from your existing identity provider, a JWT filter on your ingress or gateway is the simpler and correct tool.

[oauth2-proxy configuration](https://oauth2-proxy.github.io/oauth2-proxy/configuration/overview)

### Netlify

If you deploy on Netlify with site-level password protection

1. Password protection serves an HTML form rather than a 401, and a missing fallback rule can produce a bare 404 instead.
2. Remove site-level protection from the MCP function's path and authenticate inside the function.
3. Note that Netlify streaming functions carry a 10-second execution limit, which is a separate constraint worth checking while you are here.

[Netlify functions reference](https://docs.netlify.com/build/functions/api/)

### Something else

Any gate built for browser sessions fails the same way, so the fix has the same two shapes wherever it lives. Either exempt the MCP route from the gate and authenticate inside your server, or configure the gate to answer an unauthenticated API request with 401 rather than a redirect. Find the component that answered the `curl` above — a proxy, an ingress annotation, an identity-aware proxy, a platform setting — and change it at that layer, not in your MCP handler.

## Why it happens

Platform access controls — Vercel Deployment Protection, Azure Easy Auth, an AWS load balancer OIDC action, oauth2-proxy, a site password — assume the caller is a browser that can follow a redirect, render a login form, and come back with a cookie. An MCP client can do none of that. It expects HTTP 401, and for the spec OAuth flow it expects a `WWW-Authenticate` header telling it where to authenticate. A redirect to a login page is a 200 or 302 carrying HTML, so the client either reports a JSON parse error or a generic connection failure, and the error text rarely mentions authentication at all.

### The exchange

The MCP client posts to the server. A platform auth gate answers first with a 302 to a login page, so the exchange ends at the gate and the MCP server never sees the request. A 403 with an HTML body fails the same way.

**Participants:** MCP client, Platform auth gate (expects a browser), MCP server

1. **MCP client → Platform auth gate** — `POST /mcp` — `Accept: text/event-stream`
2. **Platform auth gate → MCP client** — `302 Found → /login` — `Content-Type: text/html` _(redirect; this step fails — the flow stops here)_

Notes:

- **Step 2** — An MCP client cannot render a login form or come back with a cookie. It needs a 401, and for the spec OAuth flow a WWW-Authenticate header naming where to authenticate. It reports a JSON parse error or a bare connection failure instead.

## What lands on you next

A login redirect meant no agent ever reached the route. Once one can, the route is a public entry point into your tools, and the questions change shape.

- **Anyone with a token can now reach every tool on this server.** — `mcp-capability-filter-inbound` — An allow-list of tools, prompts and resources per route. A call to a capability the route does not expose is refused with `MethodNotFound` before it is forwarded.
- **What stops one agent looping on an expensive tool?** — `rate-limit-inbound` — Limits counted against the authenticated identity rather than a shared egress IP, so one agent's retry storm does not spend another team's budget.
- **The platform gate used to be our audit log. What replaces it?** — `capability_invocation` — Every tool call emits an event carrying the caller, the capability, the upstream, the outcome and the latency. Tokens and request bodies are never logged.

All of these attach to one MCP route's `policies.inbound` — the same policy engine on the way in and on the way out. See [Zuplo MCP Gateway](/mcp-gateway).

## Keep reading

- **Prevent it** — [API Key Authentication for MCP Servers](/learn/mcp/authentication/api-key) — Service-to-service calls where you control both the agent and the MCP server
- **Similar error** — [`Resource server does not implement OAuth 2.0 Protected Resource Metadata.`](/learn/mcp/errors/401-without-www-authenticate) — Your server answers 401 correctly, but the response carries no pointer to protected resource metadata — so the client has nothing to discover the authorization server with, and the OAuth flow never starts.
- **Similar error** — [`MCP headers config ignored when server has OAuth discovery`](/learn/mcp/errors/configured-headers-ignored-when-oauth-discovery-present) — The credential is sitting in your client config and never reaches the wire: the client found OAuth discovery metadata on your server first, and switched to the OAuth flow instead of sending the header you set.
- **Error reference** — [every documented MCP error](/learn/mcp/errors)
