# OAuth step-up mid-call

When a tool server answers insufficient_scope, the call asks for the missing scope and resumes, instead of failing to the model.

An MCP server can refuse a call because the token lacks a scope. It answers with HTTP 403 (or 401) and a challenge like this:

```
WWW-Authenticate: Bearer error="insufficient_scope", scope="tickets.read tickets.write",
                  resource_metadata="https://wiki.example.com/.well-known/oauth-protected-resource"
```

Before this, the call waited out its timeout and the model was told only that "the tool failed". Now the challenge is read the moment it arrives, and what happens next depends on whose credential the call used.

## What happens

| The call used | What happens |
| --- | --- |
| The server's client credentials | The token is minted again with the scopes the server named, and the call is made once more. |
| An on-behalf-of exchange | The same: the platform exchanges again with the wider scope and retries once. |
| A person's own connection (browser consent) | The person gets a link to reconnect with the old and new scopes together. A run that can suspend waits for them (the same wait as any scope upgrade) and resumes on reconnect. |
| Any credential, when the server wants a stronger sign-in (`insufficient_user_authentication`, RFC 9470) | The call is refused. No scope fixes that, so nothing is learned. |

The scopes are learned for that tool only. The next call to the same tool asks for them up front, while other tools on the server keep the scopes they worked with. A server that refuses even the re-minted token fails the call after one retry, so it never loops.

Only a challenge from the server's own address counts. A host that a redirect pointed to has no say over this server's token.

A server that never sends the challenge sees no change: same config, same calls, same error text.

## See and reset what was learned

```http
GET    /api/mcp-stepup                         # settings + scopes learned per server/tool
DELETE /api/mcp-stepup/learned/{server}?tool=edit
```

## Liyagent's own /mcp

Control-plane tools on `/mcp` need `governance.read`, `governance.write` or `audit.read`. When a person's grant lacks the scope, the default answer is today's: a tool result that names the scope.

Turn on `challenge_on_mcp` to answer with HTTP 403 and the standard challenge instead:

```http
PUT /api/mcp-stepup/settings
{"challenge_on_mcp": true}
```

The challenge's `scope` lists the scopes the grant holds plus the missing one, so an MCP client can send its user through consent once and call again.

No challenge is sent to an agent credential, or to an app whose client record no longer allows the scope. Consent cannot give either of those what they lack.

## Audit

| Row | When it's written |
| --- | --- |
| `mcp.stepup.learned` | The scopes a tool was found to need. |
| `mcp.stepup.retry` | A re-minted call was made again. |
| `mcp.stepup.consent` | A person was sent to reconnect. |
| `mcp.stepup.refused` | A stronger sign-in was wanted, or the server named no scope. |
| `mcp.stepup.challenge` | /mcp issued the challenge. |
| `mcp.stepup.settings` and `mcp.stepup.forget` | An operator changed the settings or reset learned scopes. |
