---
title: "Connect a client"
description: "Point Claude Code, a claude.ai custom connector, or any MCP-capable client at your app's agent endpoint — including the tunnel you need in local dev, and what a stubborn 401 means."
canonical_url: "https://nuxt-backend.dev/docs/agents/mcp/connect-a-client"
---
# Connect a client

> Point Claude Code, a claude.ai custom connector, or any MCP-capable client at your app's agent endpoint — including the tunnel you need in local dev, and what a stubborn 401 means.

Everything a client needs is one URL:

```text
https://your-app.com/mcp
```

Transport is **HTTP** (streamable HTTP), and authorization is OAuth — the client discovers the flow from the `401` the endpoint answers with, registers itself, and sends the human off to sign in. No API key to issue, nothing to paste but the URL.

## Claude Code

```bash
claude mcp add --transport http nuxt-backend https://your-app.com/mcp
```

Then run `/mcp` inside Claude Code and pick the server to authenticate: a browser opens on your app's login page, you sign in and approve the scopes, and the tools appear. `claude mcp list` shows the server's connection state.

## claude.ai custom connector

In claude.ai, open **Settings → Connectors → Add custom connector**, give it a name, and paste `https://your-app.com/mcp`. Connecting starts the same OAuth flow in a popup. The URL must be publicly reachable over HTTPS — claude.ai runs the connection from Anthropic's servers, not from your machine.

## Any MCP-capable client

Anything that speaks MCP over HTTP with OAuth 2.1 works the same way. A client needs three things:

- HTTP transport (not stdio) pointed at `https://your-app.com/mcp`.
- Support for the `WWW-Authenticate` / protected-resource discovery handshake (RFC 9728) and the authorization-code flow.
- **Dynamic client registration** with PKCE — nothing in the app pre-registers agents, and every authorization stops at the consent page.

<tip>

For editors, `@nuxtjs/mcp-toolkit` adds two public conveniences next to the endpoint that the OAuth gate deliberately never challenges: `{route}/deeplink` (a one-click IDE install link) and `{route}/badge.svg`. When you start the dev server with Cursor or VS Code detected, it prints the install link in the terminal.

</tip>

## Local development

A client on the **same machine** can use your dev URL directly (`http://localhost:3000/mcp`, or whatever your dev server prints).

A **remote** client — claude.ai's custom connectors above all — cannot reach `localhost`, so local dev needs a public tunnel to your dev server:

```bash
ngrok http 3000                                    # or:
cloudflared tunnel --url http://localhost:3000
```

Append `/mcp` to the public URL it prints and hand that to the client.

<warning>

Whatever tunnel you use, the OAuth flow only completes when the deployment's **SITE_URL matches the origin the browser is on**. It is the only trusted browser origin for auth, so set `SITE_URL` to the tunnel's origin while you test remote clients (`npx nuxt-backend env push` after editing `.env.local`, or set it on the deployment directly) — otherwise sign-in fails mid-flow. For clients on localhost, `AUTH_TRUST_LOCAL_ORIGINS=1` on a **dev** deployment trusts `http://localhost:*` and `http://127.0.0.1:*` instead.

</warning>

## Troubleshooting a 401

Repeated `401`s with a `WWW-Authenticate` header are **the discovery handshake**, not an error — the client is meant to follow `resource_metadata` and come back with a token. Check the pieces in order:

```bash
# 1. Does the endpoint challenge correctly?
curl -i -X POST https://your-app.com/mcp \
  -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# → 401 + WWW-Authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource"

# 2. Are both discovery documents served?
curl -s https://your-app.com/.well-known/oauth-protected-resource
curl -s https://your-app.com/.well-known/oauth-authorization-server
```

<table>
<thead>
  <tr>
    <th>
      What you see
    </th>
    
    <th>
      What it means
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        401
      </code>
      
       forever, client never opens a browser
    </td>
    
    <td>
      The client doesn't implement OAuth with dynamic registration — it can't use this endpoint
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        401
      </code>
      
       right after it worked
    </td>
    
    <td>
      The agent's token was revoked, the account was banned or deleted, or <code>
        AUTH_SECRET
      </code>
      
       was rotated. Re-run the client's OAuth flow
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        401
      </code>
      
       for a while after a change
    </td>
    
    <td>
      Sessions are cached for the JWT's five-minute lifetime; revocations take effect within minutes
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        503 Agent endpoint unavailable
      </code>
    </td>
    
    <td>
      No backend site URL configured on the Nuxt side (<code>
        NUXT_PUBLIC_BACKEND_SITE_URL
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        502 Agent token exchange failed
      </code>
    </td>
    
    <td>
      <code>
        POST /mcp/exchange
      </code>
      
       isn't mounted (<code>
        registerBackendRoutes({ … mcp })
      </code>
      
      ), or the deployment is unreachable
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        429 Too many agent requests
      </code>
    </td>
    
    <td>
      The <code>
        mcp
      </code>
      
       rate limit — 60 exchanges per minute per client + user
    </td>
  </tr>
  
  <tr>
    <td>
      The second document returns <code>
        503
      </code>
      
       / <code>
        502
      </code>
    </td>
    
    <td>
      The authorization-server metadata couldn't be fetched from the deployment — check the Convex site URL and that the deployment is up
    </td>
  </tr>
  
  <tr>
    <td>
      Connects, but <code>
        tools/list
      </code>
      
       is empty
    </td>
    
    <td>
      The agent has no consented scopes, or a browser-style <code>
        GET
      </code>
      
       reached the endpoint — tools are hidden without a session
    </td>
  </tr>
  
  <tr>
    <td>
      Everything 404s
    </td>
    
    <td>
      The route was moved (<code>
        backend.mcp.route
      </code>
      
      )
    </td>
  </tr>
</tbody>
</table>

More symptoms — including what to do after rotating `AUTH_SECRET` — are in [Troubleshooting](https://nuxt-backend.dev/docs/ship/operate/troubleshooting) and the [production checklist](https://nuxt-backend.dev/docs/ship/production).

<page-keys>

- Try it live in the playground: [/playground/platform/agents](https://nuxt-backend.dev/playground/platform/agents)
- API reference: [/docs/reference/api/mcp](https://nuxt-backend.dev/docs/reference/api/mcp)
- Source: [https://github.com/qruto/nuxt-backend/blob/main/src/convex/integrations/mcp.ts](https://github.com/qruto/nuxt-backend/blob/main/src/convex/integrations/mcp.ts)

</page-keys>


## Sitemap

See the full [sitemap](https://nuxt-backend.dev/sitemap.md) for all pages.
