Agent Wranglers

AGT-32Recommended

Name every missing scope in one challenge

When a token lacks a scope a call needs, answer 403 with error="insufficient_scope", every scope the call needs in scope, and the resource_metadata address.

Spec version

MCP 2026-07-28

Review by

2027-04-30

How to do it

An agent can only ask its person for more access when it knows which scopes to ask for. A bare 403 leaves it guessing. A challenge that names one missing scope at a time sends the person through consent again and again. Work out every scope the call needs from its arguments, then list them all in one WWW-Authenticate header. Treat a broader scope as covering the narrower ones it implies. Keep offline_access out of the list, because refresh tokens are not a resource requirement. The 401 for a missing token is in describe how to get access. The person then sees the agent named on the consent screen.

Examples

HTTP example

HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="files:read files:write", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource"

JavaScript example

const RESOURCE_METADATA = 'https://mcp.example.com/.well-known/oauth-protected-resource';

export function scopeChallenge(neededScopes, grantedScopes) {
  if (neededScopes.every(scope => grantedScopes.includes(scope))) return null;
  return { status: 403, header: `Bearer error="insufficient_scope", scope="${neededScopes.join(' ')}", resource_metadata="${RESOURCE_METADATA}"` };
}

Python example

RESOURCE_METADATA = "https://mcp.example.com/.well-known/oauth-protected-resource"

def scope_challenge(needed_scopes: list[str], granted_scopes: set[str]) -> dict | None:
    if all(scope in granted_scopes for scope in needed_scopes):
        return None
    scopes = " ".join(needed_scopes)
    return {"status": 403, "header": f'Bearer error="insufficient_scope", scope="{scopes}", resource_metadata="{RESOURCE_METADATA}"'}

How to check

Call a tool that needs files:read files:write with a token that holds only files:read. The answer is 403 with error="insufficient_scope", a scope value holding both scopes, and a resource_metadata URL that answers 200.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/agents-as-users/delegated-access-and-consent/scope-challenge">Agent Wranglers agents as users playbook</a>. In code you write, add a comment next to it with the same link.