Announcing CIMD support for MCP Client registration
Learn more

MCP errors: 401, 404, invalid_grant and connection closed

MCP errors: HTTP 401 and 404 from the MCP server, -32000 Connection closed from the SDK, and invalid_grant meaning sign in again

When one of your MCP integrations fails, the error comes from one of five layers: the server process, HTTP, the JSON-RPC protocol, the OAuth token endpoint, or the app the tool calls. A 401 means the MCP server rejected your access token. A 404 with error -32601 from a current server means it does not know the method you called. A 404 with the message "Session not found" comes from an older, session-based server: the session expired, the server restarted, or a load balancer sent the request to an instance that never saw the session. A 404 with an HTML or empty body usually means the URL is wrong. invalid_grant comes from the authorization server and usually means the user has to sign in again. And "Connection closed" usually means a local server process exited or wrote something other than JSON-RPC to stdout. Each section below names the error, how to tell which layer produced it, and the fix.

TL;DR

  • Read the response, not just the status code. A 401 carries a WWW-Authenticate header, and most 404s from an MCP server carry a JSON-RPC error body. The header, the error code and the message tell you more than the status.
  • The 2026-07-28 MCP spec removed protocol sessions. Advice to "start a new session on 404" applies only to servers on the 2025-11-25 version or older.
  • -32000 and -32001 have no fixed meaning. When the message is "Connection closed" or "Request timed out", your own SDK raised the error; newer TypeScript SDK versions report these as named codes such as CONNECTION_CLOSED. The same numbers also arrive from older TypeScript SDK servers with other messages, such as a 404 "Session not found". Read the message and the HTTP status along with the number.

Find the layer first

MCP error messages rarely say which layer failed. "Connection closed" can mean a crashed process, a stray print statement or a bad path; a 401 can mean an expired token or a token minted for a different server. Before changing anything, find which layer produced the error: did the process start, did HTTP return a status, did the JSON-RPC body carry a code, or did a token endpoint answer? Each layer has its own fix. Then check which spec version a fix was written for. The July 2026 release removed protocol sessions and changed what a 404 means, so a lot of older troubleshooting advice does not apply to current servers.

Quick lookup

Error
Comes from
Usual cause
First fix
HTTP 401
MCP server
No token, expired token, or a token issued for another server
Read WWW-Authenticate; refresh or re-authorize; check the token audience
HTTP 403 insufficient_scope
MCP server
Token is valid but lacks a scope this call needs
Authorize again with the scopes named in the challenge
HTTP 403 or 421 "Invalid Host header" or "Invalid Origin header"
MCP server
DNS-rebinding protection rejected the Host or Origin, often because a proxy forwarded the public hostname
Add the hostname or origin to the server's allowed list
HTTP 404 with -32601
MCP server (2026-07-28)
Unknown method
Check the method name and that the server supports it
HTTP 404 "Session not found" (-32001 from TypeScript SDK servers, -32600 from Python SDK servers)
MCP server (2025-11-25 or earlier)
Session expired, the server restarted, or the load balancer sent you to another instance
Initialize again; on a multi-instance deployment, use sticky sessions or run the server stateless
HTTP 404 with HTML or an empty body
Proxy or wrong URL
Wrong path, a stripped path prefix, or an HTTP+SSE-only server
Check the endpoint path (/mcp vs /sse)
HTTP 400 "Server not initialized" or "Mcp-Session-Id header is required" (-32000)
Older TypeScript SDK server
A stateful server on serverless or behind a load balancer without sticky sessions
Use sticky sessions or run the server stateless
HTTP 400 with -32020 or -32022
MCP server (2026-07-28)
Protocol version header missing, mismatched or unsupported
Upgrade the client SDK; check data.supported
HTTP 405
MCP server
GET or DELETE the server does not support (always the case on 2026-07-28 servers)
Use a client that speaks the server's transport version
invalid_grant
Authorization server
Refresh token or code expired, revoked or issued to another client
Stop retrying; check the redirect URI and client, then ask the user to sign in again
-32000 Connection closed
Your SDK
stdio server exited, wrote to stdout, or could not start; for a remote server, a proxy closed the stream
Read the server's stderr; run it in the Inspector
-32001 Request timed out
Your SDK
No response within the timeout (60 seconds by default in the TypeScript SDK)
Raise the timeout or make the tool return sooner

401 Unauthorized

Under the MCP authorization spec, invalid or expired tokens MUST get a 401, and so must tokens that were not issued for this server. The response usually points to the server's protected resource metadata, and RFC 6750 adds an error code when a token was sent:

HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer error="invalid_token", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource/mcp"

Work through the causes in this order:

  1. No token was sent. The challenge has no error code. The client should start the OAuth flow; in Claude Code, /mcp shows the server as needing authentication.
  2. The token expired. error="invalid_token". Refresh it once and retry once.
  3. The token is for a different server. Servers MUST check the audience. If your client did not send the resource parameter, or sent the wrong URL, the token's audience will not match. Check that the token's aud names this server: its canonical URI, including the path, or a resource ID if your authorization server uses one.
  4. The token is fine but the server is misconfigured. An issuer or audience mismatch in the server's own validation code gives a 401 on every call even after a successful sign-in. Our MCPJam debugging guide walks through this case.

In Claude Code, a 401 from an OAuth server you already signed in to makes it refresh the token, reconnect and retry once; it flags the server in /mcp only if that retry also fails. Two cases look like a valid token being rejected. If you set headers.Authorization yourself and the server rejects it, Claude Code reports the server as failed, not as needing authentication, and does not fall back to OAuth. And in a remote server's url and headers, Claude Code reads some credential variables, such as ${ANTHROPIC_AUTH_TOKEN}, as empty, so the server receives Bearer with no token and returns 401. Copy the value into a variable with a name of your own. To sign in from the shell, run claude mcp login <name>.

A few 404s during sign-in are normal. If the 401 has no resource_metadata, clients try /.well-known/oauth-protected-resource/mcp and then the root path, and authorization server discovery then tries several OAuth and OpenID Connect well-known URLs in order. A 404 on an early URL followed by a 200 on a later one is the documented fallback.

403 insufficient_scope

A 403 means the token is valid but not enough for this call. The server SHOULD return error="insufficient_scope" with every scope the call needs. The client should ask the user to authorize again for the combined scopes, with a cap on retries, then give up and report the error. If the same 403 keeps coming back after the user approves, check that your client requests the scopes the server named in the challenge.

Two other 403s have nothing to do with scope. A 403 with "Invalid Origin header" or "Invalid Host header" comes from DNS-rebinding protection; the spec requires servers to reject an invalid Origin with a 403. Python SDK servers answer a bad Host with a 421 "Invalid Host header" instead, usually because a reverse proxy forwarded the public hostname; add it with TransportSecuritySettings(allowed_hosts=[...]). Atlassian's token endpoint also uses 403, with invalid_grant, for an unknown or invalid refresh token (see below).

404 Not Found

What a 404 means depends on the spec version the server implements.

Current servers (2026-07-28). Protocol sessions are gone, so there is no session to expire. A 404 with a JSON-RPC body means the method is unknown (Streamable HTTP):

HTTP/1.1 404 Not Found Content-Type: application/json {"jsonrpc": "2.0", "id": 7, "error": {"code": -32601, "message": "Method not found"}}

Older servers (2025-11-25 and earlier). A 404 in response to a request with an Mcp-Session-Id header means the session has ended, and the client MUST start a new one with a fresh initialize request (2025-11-25 transports). Servers built on the official SDKs send this 404 with a JSON-RPC body whose message is "Session not found". The code is -32001 from TypeScript SDK servers and -32600 from Python SDK servers:

HTTP/1.1 404 Not Found Content-Type: application/json {"jsonrpc": "2.0", "id": null, "error": {"code": -32600, "message": "Session not found"}}

The session expired, the server restarted, or the request reached a different instance. Python SDK servers drop a session after 30 minutes with no request in flight (session_idle_timeout). If the error appears without a restart or a long idle gap, the usual cause is several workers behind a load balancer without sticky sessions: each worker keeps its own session table, so a request routed to another worker finds no session. Fix it with sticky routing, or run the server stateless (stateless_http=True in the Python SDK).

TypeScript SDK servers show the same deployment problem as a 400 with code -32000 and the message "Bad Request: Server not initialized" or "Bad Request: Mcp-Session-Id header is required". Both mean the request reached an instance that never handled the initialize request, which is common for a stateful server on serverless functions. The fix is the same.

Either version. A 404 with an HTML body or no body usually means the URL is wrong. Check the path, any trailing slash, and whether a reverse proxy strips the path before it reaches the server. Most servers serve /mcp. A URL ending in /sse belongs to the HTTP+SSE transport, which has been deprecated since 2025-03-26 and is formally Deprecated in 2026-07-28; configure the client for SSE (for example claude mcp add --transport sse) or move to the server's /mcp endpoint. Clients use the shape of these errors to tell protocol versions apart: a 400, 404 or 405 without a recognised JSON-RPC body lets a client that also supports older versions fall back to them.

400 and 405 on current servers

Every request to a 2026-07-28 server carries an MCP-Protocol-Version header that must match the version in the request body. A missing or mismatched header gets a 400 with -32020 (header mismatch). An unsupported version gets a 400 with -32022, and data.supported lists the versions the server accepts. A request that is missing required metadata gets -32602. The fix in all three cases is usually a newer client SDK.

A 405 means the client sent GET or DELETE. Older clients used GET for a server-to-client stream and DELETE to end a session; a current server should answer both with 405. Older servers may also return 405 to a GET when they do not offer a server-to-client stream, or to a DELETE when they do not let clients end sessions.

invalid_grant and invalid_client

These come from the authorization server's token endpoint, not from the MCP server. invalid_grant means the authorization code or refresh token is invalid, expired or revoked, the redirect URI does not match, or the token was issued to a different client (RFC 6749). Retrying will not fix it. Check the redirect URI and client configuration first; if those are right, the user has to sign in again.

Each provider has its own reasons for killing a refresh token, and they are worth knowing before you ask a user to reconnect:

Provider
Refresh token lifetime
Other causes of invalid_grant
Google
Expires after six months unused, or after 7 days if the OAuth consent screen is external and set to Testing (unless the app asks only for name, email and profile scopes)
The user revoked access or changed their password and the token has Gmail scopes; more than 100 live refresh tokens per Google Account per client ID (a new one silently invalidates the oldest); time-based access ended; an admin set a requested service to Restricted (admin_policy_enforced)
Microsoft Entra
90 days by default; 24 hours for single-page apps
A user password change for password-based sign-ins, an admin password reset in the Entra or Microsoft 365 admin center, or the user or an admin revoking refresh tokens
Atlassian
Rotating refresh tokens expire after 90 days of inactivity
The app kept using a refresh token that had already been replaced (outside a 10-minute reuse window), or the user changed their password. Atlassian returns these as a 403 rather than the usual 400, with "Unknown or invalid refresh token."

Microsoft and Atlassian return a new refresh token with every refresh. Store it each time: Atlassian invalidates the old one, so a client that keeps using it gets invalid_grant.

invalid_client means the authorization server does not accept your client's credentials. For a client with a fixed client ID and secret, they are wrong, rotated or deleted. An MCP client that registered itself, through dynamic client registration or a client ID metadata document, gets it when the authorization server dropped the registration, could not fetch or validate the metadata document, or changed. The 2026-07-28 spec requires clients to key stored credentials by the authorization server's issuer and to register again with a new one. Clear the client's stored authorization so it registers again; in Claude Code, open /mcp and choose Clear authentication.

For the full refresh and re-consent pattern, see token refresh for third-party MCP servers.

Connection closed (-32000)

This message comes from the client SDK when the transport closes. With a local (stdio) server, the common causes are that the process exited or its output stream broke. The stdio spec is strict: a server MUST NOT write anything to stdout that is not a valid MCP message, so a single print statement is enough to break the connection.

import logging, sys # In a stdio server, stdout carries JSON-RPC. Send everything else to stderr. logging.basicConfig(stream=sys.stderr, level=logging.INFO) log = logging.getLogger("my-mcp-server") log.info("starting") # fine # print("starting") # breaks the connection: not a valid MCP message
// stdout is the protocol channel in a stdio server console.error("starting"); // fine, goes to stderr // console.log("starting"); // corrupts the JSON-RPC stream

If logging is not the problem, check these, from the MCP debugging guide:

  • The command path. The client launches the server itself. Use an absolute path to the executable and to any files it reads, since the working directory may be / on macOS.
  • Environment variables. stdio servers inherit only a limited environment. Pass API keys and other settings in the server's env block.
  • The config file. Invalid JSON in the client config stops the server from starting at all.
  • The real error. SDKs often report a generic "Connection closed" while the real cause is only in the transport's error handler or on the server's stderr; an open TypeScript SDK issue tracks the error-handler case. Check both before changing code.

With a remote server, one common cause is a proxy or load balancer closing a long-running request. Check its timeout settings. The 2026-07-28 spec removed stream resumability, so when a response stream drops, the in-flight request is lost and the client must send it again with a new request ID.

Request timed out (-32001)

Neither -32000 nor -32001 is defined by the MCP spec, which says local SDK errors such as timeouts are not assigned codes. The Python SDK uses -32001 for a request timeout, and the TypeScript SDK's default request timeout is 60 seconds. If -32001 arrives on an HTTP 404 with the message "Session not found", it is a session error; see the 404 section.

If a tool legitimately takes longer, raise the timeout or have the tool return sooner. Progress notifications do not always help: the TypeScript SDK resets its timer on progress only when the caller sets resetTimeoutOnProgress, and Claude Code's per-server timeout is a hard limit that progress does not extend. The settings to change are these:

  • Claude Code. MCP_TIMEOUT sets how long a server has to start. MCP_TOOL_TIMEOUT, or a per-server "timeout" in .mcp.json, sets how long a tool call may run (Claude Code MCP docs).
  • Codex. tool_timeout_sec for tool calls and startup_timeout_sec for startup.
  • Your own TypeScript client. The timeout and resetTimeoutOnProgress request options.

Tool errors vs protocol errors

The tools spec separates two kinds of tool failure. A protocol error, such as an unknown tool name or a request that does not match the tools/call schema, is a JSON-RPC error, usually -32602. An execution error, such as an argument that fails validation or an upstream API failure, comes back as a normal result with isError: true, so the model can read it and try something else. If your server returns API failures as JSON-RPC errors, the model may never see the message; return them as tool results with a short, specific explanation instead. Tool call failures in production covers how to classify them.

Code
Meaning
Defined by
-32700
Parse error: the body is not valid JSON
JSON-RPC 2.0
-32600
Invalid request
JSON-RPC 2.0
-32601
Method not found
JSON-RPC 2.0
-32602
Invalid params; also unknown tool, missing metadata, and resource not found in 2026-07-28
JSON-RPC 2.0, used by MCP
-32603
Internal error
JSON-RPC 2.0
-32002
Resource not found in older versions; replaced by -32602, but clients should still accept it
MCP, before 2026-07-28
-32020 to -32022
Header mismatch, missing client capability, unsupported protocol version
MCP 2026-07-28

Debugging tools

The MCP Inspector connects to a server directly, without an AI client in between, so you can see the raw requests and responses. It needs Node.js 22.19 or later.

# Local stdio server npx @modelcontextprotocol/inspector node build/server.js # Remote server npx @modelcontextprotocol/inspector --server-url https://mcp.example.com/mcp --transport http

Claude Code. /mcp shows each server as connected, needing authentication, or failed, and lets you authenticate, clear authentication or reconnect. For a server that failed to connect, claude mcp get shows an Issue line with the HTTP status or error code (Claude Code v2.1.219 or later). claude mcp login <name> runs a server's OAuth sign-in from the shell.

# Longer startup timeout (milliseconds) and MCP debug logs MCP_TIMEOUT=10000 claude --debug=mcp,startup # Status and last error for one server claude mcp get my-server

Cursor. Servers are configured in .cursor/mcp.json in the project or ~/.cursor/mcp.json. For logs, open the Output panel (Cmd+Shift+U) and choose "MCP Logs". The desktop app's OAuth redirect URL is http://localhost:8787/callback, which is the value to check when an authorization server reports redirect_uri_mismatch.

Codex. Servers live in ~/.codex/config.toml under [mcp_servers.<name>]. startup_timeout_sec defaults to 10 seconds, which a server launched with npx -y can exceed while it downloads on first run, and tool_timeout_sec defaults to 60. Run codex mcp login <name> for an OAuth server, or set bearer_token_env_var to send a static token from an environment variable.

VS Code. Run MCP: List Servers and choose Show Output. Claude Desktop on macOS writes logs to ~/Library/Logs/Claude/mcp*.log.

Errors from Scalekit AgentKit

If your agent calls tools through Scalekit, a few errors are specific to how it holds each user's credentials. The AgentKit troubleshooting guide covers each one:

What you see
Meaning
Fix
401 on a Scalekit MCP server URL
The session token expired, was minted for a different server, or was copied incompletely
Mint a new session token for this server and user. Session tokens are short-lived by design, so mint a new one per run or on a schedule
REAUTHENTICATION_NEEDED or UNAUTHENTICATED
The user's connected account is no longer usable, for example EXPIRED
Send the user a new authorization link; a new Scalekit token will not help
failed_to_exchange_token
The app's token endpoint returned an error such as invalid_grant or invalid_client
Check the connection's OAuth credentials, then have the user reconnect
Account keeps returning to EXPIRED
The app never issued a refresh token
Add the offline access scope (for example offline_access) to the connection

To catch these before a user does, subscribe to the connected_account.token_refresh_failed and connected_account.status_updated webhooks, and send a reconnect link when an account changes state.

FAQs

Why does my MCP server return 401?

The server rejected the access token: none was sent, it expired, or it was issued for a different server. Read the WWW-Authenticate header, refresh or re-authorize, and check that the token's audience matches the server's URL.

Why does my MCP server return 404?

On a 2026-07-28 server, a 404 with error -32601 means the method is unknown. A 404 whose message is "Session not found" comes from an older, session-based server: the session expired, the server restarted or a load balancer routed the request to another instance, so initialize again and use sticky sessions or a stateless server. A 404 with an HTML or empty body usually means the URL path is wrong.

What does MCP error -32000 Connection closed mean?

The client SDK lost the connection to the server. For a local stdio server, the usual causes are the process exiting, the server writing logs to stdout, a wrong command path or missing environment variables. Check the server's stderr first.

What does invalid_grant mean for an MCP server?

The authorization server rejected a refresh token or authorization code because it is invalid, expired, revoked or belongs to another client. Retrying will not help. If the redirect URI and client configuration are correct, the user needs to sign in again.

How do I debug an MCP server?

Connect to it with the MCP Inspector to see raw requests and responses, without an AI client in between. In Claude Code, use /mcp, claude mcp get, and the --debug flag, and raise MCP_TIMEOUT if the server is slow to start.

No items found.
Agent
Auth Quickstart
On this page
Share this article
Agent
Auth Quickstart

Acquire enterprise customers with
‍zero upfront cost.

Every feature unlocked. No hidden fees.