Lazy authentication for MCP servers changedconnectors/building/lazy-authentication
Nearest release: v2.1.283, published under an hour after upstream edited the page. Shown because the two are within 24 hours of each other. Nothing here says the release caused the edit.
Upstream edited this page at 25 Sep 2026 18:00 UTC, give or take a minute or two: the time comes from Anthropic’s own sitemap rather than from a commit. This site recorded the change at 20 Oct 2026 18:07 UTC.
Upstream edited
Recorded here
Lines+73added
Lines−75removed
From line
17
where the diff opens
First seen
14 Aug 2026
this site's first read of the page
Recorded edits4to this page, all time
The whole hunk
from line 17, old and new numbered
/
from line 17
17173. They click **Connect** and sign in to your service in a popup.
18184. Claude retries the same tool call automatically with the new token, and the turn continues with no context lost.
1919
20<Note>
21 If the user's organization has [Enterprise Managed Auth](/docs/connectors/building/enterprise-managed-auth) configured for your connector, the same refusal triggers a silent token exchange instead of the **Connect** card. Claude retries the tool call automatically and the user sees no prompt.
22</Note>
20> [!NOTE]
21>
22> If the user's organization has [Enterprise Managed Auth](/docs/connectors/building/enterprise-managed-auth) configured for your connector, the same refusal triggers a silent token exchange instead of the **Connect** card. Claude retries the tool call automatically and the user sees no prompt.
2323
2424## Build lazy authentication on your server
2525
from line 136
136136{"jsonrpc":"2.0","result":{"isError":true,"content":[{"type":"text","text":"Please sign in"}]},"id":1}
137137```
138138
139<Warning>
140 A `200` with `isError: true` is an application-level tool failure. Claude passes the error text to the model as the tool result and moves on, and there is no auth prompt. Only a transport-level `401` causes Claude to pause the call, run the OAuth flow, and retry. A `403` triggers re-authentication only when accompanied by `WWW-Authenticate: Bearer error="insufficient_scope"` for [scope step-up](#ask-for-more-scope-with-403), and any other `403` is surfaced as a terminal error. If users are seeing "please sign in" text in the chat instead of a **Connect** button, the server is returning the wrong one.
141</Warning>
139> [!WARNING]
140>
141> A `200` with `isError: true` is an application-level tool failure. Claude passes the error text to the model as the tool result and moves on, and there is no auth prompt. Only a transport-level `401` causes Claude to pause the call, run the OAuth flow, and retry. A `403` triggers re-authentication only when accompanied by `WWW-Authenticate: Bearer error="insufficient_scope"` for [scope step-up](#ask-for-more-scope-with-403), and any other `403` is surfaced as a terminal error. If users are seeing "please sign in" text in the chat instead of a **Connect** button, the server is returning the wrong one.
142142
143143### Serve the discovery documents
144144
from line 203
203203}
204204```
205205
206<Note>
207 Claude selects CIMD only when the authorization-server metadata advertises both `client_id_metadata_document_supported: true` and `"none"` in `token_endpoint_auth_methods_supported`. The second is required because Claude's CIMD client authenticates as a public client with `token_endpoint_auth_method: "none"`, so the token endpoint must accept [PKCE](https://datatracker.ietf.org/doc/html/rfc7636)-only requests without a client secret. If either property is missing, Claude falls back to looking for a `registration_endpoint`.
208</Note>
206> [!NOTE]
207>
208> Claude selects CIMD only when the authorization-server metadata advertises both `client_id_metadata_document_supported: true` and `"none"` in `token_endpoint_auth_methods_supported`. The second is required because Claude's CIMD client authenticates as a public client with `token_endpoint_auth_method: "none"`, so the token endpoint must accept [PKCE](https://datatracker.ietf.org/doc/html/rfc7636)-only requests without a client secret. If either property is missing, Claude falls back to looking for a `registration_endpoint`.
209209
210210If you move to a real issuer, keep `client_id_metadata_document_supported: true` in that issuer's metadata if you want registration-free onboarding for Claude clients.
211211
from line 246
246246
247247You can confirm the public and protected paths with `curl` and Claude Code against your own server before connecting it to Claude as a custom connector, then check the sign-in prompt in a conversation. The commands below use `http://localhost:3000/mcp` as the server URL and the example's tool names; substitute your own.
248248
249<Steps>
250 <Step title="Call a public tool without a token">
251 In a terminal, send a `tools/call` for a public tool with no `Authorization` header, with `-i` so the status line prints:
249**1. Call a public tool without a token**
252250
253 ```bash theme={null}
254 curl -si http://localhost:3000/mcp \
255 -H 'Content-Type: application/json' \
256 -H 'Accept: application/json, text/event-stream' \
257 -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_products","arguments":{}}}'
258 ```
251In a terminal, send a `tools/call` for a public tool with no `Authorization` header, with `-i` so the status line prints:
259252
260 The server answers `200 OK` with the JSON-RPC tool result and no `WWW-Authenticate` header.
261 </Step>
253```bash theme={null}
254curl -si http://localhost:3000/mcp \
255 -H 'Content-Type: application/json' \
256 -H 'Accept: application/json, text/event-stream' \
257 -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"list_products","arguments":{}}}'
258```
262259
263 <Step title="Call a protected tool without a token">
264 Send the same request for a protected tool:
260The server answers `200 OK` with the JSON-RPC tool result and no `WWW-Authenticate` header.
265261
266 ```bash theme={null}
267 curl -si http://localhost:3000/mcp \
268 -H 'Content-Type: application/json' \
269 -H 'Accept: application/json, text/event-stream' \
270 -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_my_orders","arguments":{}}}'
271 ```
262**2. Call a protected tool without a token**
272263
273 The server answers `401 Unauthorized` with a `WWW-Authenticate` header carrying `resource_metadata`. If you see `200` with `isError: true` instead, your server is returning a tool error rather than failing the HTTP request.
274 </Step>
264Send the same request for a protected tool:
275265
276 <Step title="Check both paths from Claude Code">
277 If you have [Claude Code](https://code.claude.com/docs/en/setup) installed and signed in, you can see how a Claude client treats each path before you set up a tunnel, because Claude Code connects to a `localhost` server directly. In your terminal, from your server's project folder, add the server:
266```bash theme={null}
267curl -si http://localhost:3000/mcp \
268 -H 'Content-Type: application/json' \
269 -H 'Accept: application/json, text/event-stream' \
270 -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_my_orders","arguments":{}}}'
271```
278272
279 ```bash theme={null}
280 claude mcp add --transport http authtest http://localhost:3000/mcp
281 ```
273The server answers `401 Unauthorized` with a `WWW-Authenticate` header carrying `resource_metadata`. If you see `200` with `isError: true` instead, your server is returning a tool error rather than failing the HTTP request.
282274
283 Then check the connection:
275**3. Check both paths from Claude Code**
284276
285 ```bash theme={null}
286 claude mcp list
287 ```
277If you have [Claude Code](https://code.claude.com/docs/en/setup) installed and signed in, you can see how a Claude client treats each path before you set up a tunnel, because Claude Code connects to a `localhost` server directly. In your terminal, from your server's project folder, add the server:
288278
289 The list shows `authtest: http://localhost:3000/mcp (HTTP) - ✔ Connected` with no sign-in, because connecting and listing tools never reach the gate. Next, have Claude call the public tool with [`claude -p`](https://code.claude.com/docs/en/headless), which sends one prompt and prints the answer:
279```bash theme={null}
280claude mcp add --transport http authtest http://localhost:3000/mcp
281```
290282
291 ```bash theme={null}
292 claude -p "List the products from the authtest server." --allowedTools mcp__authtest__list_products
293 ```
283Then check the connection:
294284
295 Claude reports the products and no sign-in prompt appears. Run the same command asking for your orders with `--allowedTools mcp__authtest__get_my_orders`. The tool result Claude receives is `MCP server "authtest" needs you to sign in again (run /mcp to re-authenticate)` rather than order data.
285```bash theme={null}
286claude mcp list
287```
296288
297 To sign in from Claude Code, run `/mcp` in an interactive session as [Authenticate with remote MCP servers](https://code.claude.com/docs/en/mcp#authenticate-with-remote-mcp-servers) describes. When you're done testing, run `claude mcp remove authtest`.
298 </Step>
289The list shows `authtest: http://localhost:3000/mcp (HTTP) - ✔ Connected` with no sign-in, because connecting and listing tools never reach the gate. Next, have Claude call the public tool with [`claude -p`](https://code.claude.com/docs/en/headless), which sends one prompt and prints the answer:
299290
300 <Step title="Add the server to Claude as a custom connector">
301 Claude reaches custom connectors from Anthropic's infrastructure, so `localhost` isn't reachable directly. To add a local server:
291```bash theme={null}
292claude -p "List the products from the authtest server." --allowedTools mcp__authtest__list_products
293```
302294
303 1. Expose the server over a public HTTPS tunnel, such as `cloudflared tunnel --url http://localhost:3000` or `ngrok http 3000`. Keep the tunnel up only while you test, because it exposes your local server publicly and the example's `/authorize`, `/token`, and `isTokenValid()` are stubs that treat anyone as signed in.
304 2. In Claude, go to [**Customize > Connectors**](https://claude.ai/customize/connectors) and select **Add custom connector**.
305 3. Enter the tunnel's `/mcp` URL.
295Claude reports the products and no sign-in prompt appears. Run the same command asking for your orders with `--allowedTools mcp__authtest__get_my_orders`. The tool result Claude receives is `MCP server "authtest" needs you to sign in again (run /mcp to re-authenticate)` rather than order data.
306296
307 See [Test a local server](/docs/connectors/building/testing#test-a-local-server) for details. If your app comes from the SDK's `createMcpExpressApp()`, that section also shows the `allowedHosts` option you need before requests through the tunnel succeed.
308 </Step>
297To sign in from Claude Code, run `/mcp` in an interactive session as [Authenticate with remote MCP servers](https://code.claude.com/docs/en/mcp#authenticate-with-remote-mcp-servers) describes. When you're done testing, run `claude mcp remove authtest`.
309298
310 <Step title="Try both paths in a conversation">
311 1. Ask Claude to list products. No sign-in prompt appears.
312 2. Ask for your orders. The inline **Connect** card appears, and after you authenticate the same call completes.
313 </Step>
314</Steps>
299**4. Add the server to Claude as a custom connector**
300
301Claude reaches custom connectors from Anthropic's infrastructure, so `localhost` isn't reachable directly. To add a local server:
302
3031. Expose the server over a public HTTPS tunnel, such as `cloudflared tunnel --url http://localhost:3000` or `ngrok http 3000`. Keep the tunnel up only while you test, because it exposes your local server publicly and the example's `/authorize`, `/token`, and `isTokenValid()` are stubs that treat anyone as signed in.
3042. In Claude, go to [**Customize > Connectors**](https://claude.ai/customize/connectors) and select **Add custom connector**.
3053. Enter the tunnel's `/mcp` URL.
306
307See [Test a local server](/docs/connectors/building/testing#test-a-local-server) for details. If your app comes from the SDK's `createMcpExpressApp()`, that section also shows the `allowedHosts` option you need before requests through the tunnel succeed.
308
309**5. Try both paths in a conversation**
310
3111. Ask Claude to list products. No sign-in prompt appears.
3122. Ask for your orders. The inline **Connect** card appears, and after you authenticate the same call completes.
315313
316314## Next steps
317315
No line in this hunk matches that.