Guides

    Debugging an MCP connection that won't authorize

    Most failed MCP connections are authorization problems with a short list of causes. Work this checklist before reinstalling anything.

    Versely Team9 min read

    Reinstalling Claude will not fix a Versely MCP connection that will not authorize. Neither will deleting node_modules, nor pasting the URL a second time into a field that already has it. Almost every failure in this path is one of: the wrong resource URL, an authorize request that expired, a redirect URI the server will not accept, PKCE missing or not S256, a code reused or timed out, a resource that does not match the MCP audience, a token that lacks generate, or a stdio key that is the wrong credential shape for the client you are in.

    Work the list in order. The first hit is usually the last.

    Confirm you are on the authorize path at all

    The hosted server is https://mcp.versely.studio/mcp. That path is the MCP resource. The authorization server is https://api.versely.studio. Clients are supposed to fetch https://api.versely.studio/.well-known/oauth-authorization-server and then follow authorization_endpoint, token_endpoint, and registration_endpoint from the document. If you typed the API host into the connector field, or omitted /mcp, or used http://, you are not where the token's aud will point.

    The MCP setup is three steps: Settings → Connectors, name it Versely, paste that URL, Connect, sign in. If Connect never opens a Versely page, stop. The client never reached /oauth/authorize. Fix the URL. Do not debug scopes yet.

    If you are not in a connector UI at all (you are in Claude Desktop, Cursor, Claude Code), you may not be on OAuth. Those agents often spawn a local stdio command that sends an API key. An OAuth error in a log next to a vsk_ key in claude_desktop_config.json means you are debugging the wrong door. Skip to the stdio section.

    The consent window is ten minutes, the code is five, and both are single-shot

    GET /oauth/authorize parks the request for ten minutes, then 302s you to https://app.versely.studio/oauth/consent?state=…. If you wander off, the consent page's fetch to /oauth/consent-info returns expired_or_unknown. The copy on that page is accurate: go back to the client and click Connect again. There is no way to revive a state id.

    You must be signed in as a Versely user on that consent page. The decision endpoint requires a session JWT; without one it returns login_required and no code is minted. Signing into Claude is not signing into Versely.

    Clicking Allow mints a hashed, one-time code with a five-minute lifetime and redirects to the client's redirect_uri. Failures from here have exact strings:

    What you did What the token endpoint returns
    Typo'd or lost the code invalid_grant / Code not found
    Client exchanged it twice invalid_grant / Code already used
    Waited more than five minutes invalid_grant / Code expired
    client_id does not match the code invalid_grant / client_id mismatch
    redirect_uri does not match authorize invalid_grant / redirect_uri mismatch
    code_verifier does not match the challenge invalid_grant / PKCE verification failed
    resource is a real URL that is not the MCP server invalid_target / resource mismatch

    redirect_uri is checked twice: it must be in the client's registered list (from dynamic registration) and it must start with an allowed prefix (https://claude.ai/, https://claude.com/, https://platform.claude.com/, or localhost). "redirect_uri host is not allowed" means the client is sending a callback host Versely will not send codes to. That is not something you fix by reinstalling. It is a client that is not on the allowlist, or a custom redirect you pointed at a host we do not serve.

    PKCE is not optional. Authorize without code_challenge and code_challenge_method=S256 400s with a missing-parameters message before consent. A client that implements "plain" PKCE, or no PKCE, cannot connect. S256 only.

    Token, audience, and scope: the 401 vs 403 split

    A successful exchange issues:

    • Access JWT, iss=https://api.versely.studio, aud=https://mcp.versely.studio/mcp, one hour
    • Refresh token, 30 days, rotated on every use

    Presenting that access token as Authorization: Bearer is how MCP calls are authed. A 401 here is usually expiry or a malformed JWT. Refresh. If refresh returns Refresh token revoked, you reused a token after rotation (two clients sharing one refresh, or a retry). Connect again; do not keep sending the old refresh.

    A 403 OAuth token lacks required scope: generate is not a broken connection. The connection worked. Consent granted a token that does not include generate. That happens when the client requested nothing (the server falls back to read) or requested a subset that omitted generate. read can list agentic routes and the catalog. It cannot create media. Reconnect with a client that asks for generate or all. There is no way to add a scope to an existing token; refresh may only narrow.

    If generate calls 402, that is credits, not auth. Top up on pricing. A 402 is a balance problem.

    The stdio fork: you are holding a key, not a code

    Claude Desktop's in-app recipe is a local process:

    {
      "mcpServers": {
        "versely": {
          "command": "npx",
          "args": ["-y", "mcp-remote", "https://mcp.versely.studio/mcp", "--header", "Authorization:Bearer vsk_…"],
          "env": {
            "VERSELY_AUTH": "Bearer vsk_…"
          }
        }
      }
    }
    

    That is not OAuth. Failures look like "server spawned, tools missing" or 401s in the MCP log. Check, in order:

    1. The key starts with vsk_. A value you invented, a JWT you pasted from the OAuth flow, or a key you already revoked will not do.
    2. The raw key was copied at creation. The API shows it once. If you only have a key_prefix, mint a new key.
    3. Scopes on the key include what you are calling. A read-only key will authorize the process and fail generate the same way a read OAuth token does.
    4. npx can run. A sandbox that blocks child processes, or a machine without Node, will never reach Versely. The log will blame the command, not auth.
    5. You did not commit the file. If you did, revoke the key.

    The CLI path (npm install -g @versely/cli, versely auth login, versely install) writes this shape for you and keeps the key out of a prompt. If stdio auth is the problem and you are tired of JSON, use that, then open the agent it wired. The developer page is the same three tabs.

    Do not put an OAuth access token in Authorization:Bearer in that JSON. It expires in an hour and it is not an API key. Do not put a vsk_ key into claude.ai's connector field. The connector field is a URL; the client will try to run OAuth against it.

    The ordered checklist

    1. URL is exactly https://mcp.versely.studio/mcp. Wrong host or missing /mcp: fix it, Connect again.
    2. Are you in a remote connector, or a local stdio agent? Connector: stay on OAuth. Desktop/CLI agent: stay on the key. Do not mix.
    3. Did a Versely consent page open within ten minutes of Connect? If it says expired, Connect again. If it never opened, the client never hit /oauth/authorize.
    4. Did you sign in to Versely on that page? Claude login is not enough.
    5. Allow, and let the client exchange the code once, immediately. Five minutes, one use. Retries of the same code fail on purpose.
    6. Redirect host is Claude or localhost. Anything else is "host is not allowed."
    7. PKCE S256. If the client cannot do it, that client cannot use this server.
    8. 403 on generate: look at scope. Need generate. Reconnect; do not reinstall.
    9. 401 after an hour: refresh, or Connect again. Access tokens are one hour. Refresh tokens rotate.
    10. Stdio: new vsk_ key, right scopes, npx actually runs. Revoke the old key if it leaked.

    If you have walked that list and the client still never reaches consent, it is not speaking this server's authorization spec (OAuth 2.1, dynamic registration, S256 PKCE). Pick a client that does, or use the CLI-wired stdio path. Reinstalling the same client does not add PKCE.

    The MCP vs API vs web UI comparison is the "should I even be on MCP" question. Support is the place for an outage, not for a Code already used.

    FAQ

    Connect works, then dies an hour later. Is the server dropping me?

    The access token expired. One hour is the TTL. A client that implements refresh will not bother you; a client that does not will look disconnected. Click Connect again, or use a client that stores and rotates the refresh token. Do not treat this as a flaky host.

    I clicked Deny by mistake. How do I undo it?

    You do not undo Deny. You Connect again and click Allow. Deny issues no tokens and does not blacklist you. The next authorize request is a new state id.

    Why does localhost work in the inspector but not my deployed client?

    Localhost http is allowed as a redirect prefix. A deployed client's https://my-app.example is not on the allowlist, so authorize 400s with "redirect_uri host is not allowed" even if dynamic registration accepted the URI. Registration checks https-or-localhost; authorize checks the prefix list. The inspector on localhost clears the first bar. Your deployed host has to be one of the Claude prefixes, or you need a different auth path (an API key via stdio or REST).

    The logs say resource mismatch and I never typed a resource.

    Some clients send resource=undefined as a string, or send a trailing-slash variant of the MCP URL. The token endpoint ignores values that are not an http(s) URL, and it normalises trailing slashes and case on real URLs. A mismatch means the client sent a different URL than https://mcp.versely.studio/mcp. Point the client at that resource, not at the API host, not at a docs page, not at /mcp/ on a different subdomain.