Skip to content

fix(api): explain why Cloudflare refused a request - #267

Open
mattzcarey wants to merge 1 commit into
feat/forge-toolsfrom
fix/explain-api-refusals
Open

mattzcarey wants to merge 1 commit into
feat/forge-toolsfrom
fix/explain-api-refusals

Conversation

@mattzcarey

@mattzcarey mattzcarey commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Second of five stacked PRs, on #247. Replaces #260 (same change, ported onto the Forge artifacts).

Cloudflare answers three different problems with the same 403:

execute reduced all three to Cloudflare API error: 10000: Authentication error. Agents read that as a signed-out connection, ask the user to reconnect, get the same error, and ask again. This PR makes the tool result say which case it is.

Before
Error: Cloudflare API error: 10000: Authentication error

After
Error: Cloudflare API error: GET /zones/023e…/dns_records returned HTTP 403: 10000: Authentication error
(https://developers.cloudflare.com/api/resources/dns/subresources/records/). This connection was not
granted a scope this endpoint accepts. Reconnect and grant one of: `dns.read` (DNS Read), `dns.write` (DNS Write).
Connection Endpoint accepts Next step
OAuth, holds none of the accepted scopes dns.read, dns.write Reconnect and grant one of them
OAuth, holds dns.read dns.read, dns.write Reconnecting won't help: check the role or the granted accounts
OAuth Billing Read (no OAuth scope) Use an API token with Billing Read
API token or direct Cloudflare OAuth token DNS Read, DNS Write The token needs one of these
Any nothing in the spec HTTP 403 isn't a sign-in problem; check the docs
  • What the endpoint accepts: Forge's x-api-token-group lists the API token permissions an operation accepts, and OAuth scopes use the same names.
    • The build now keeps it in spec.json (scripts/generator/spec.ts), so search can show it too.
    • Endpoint tools already carry it in their mcp-tools.json entry, and use that entry's permissions and path template.
    • execute parses spec.json paths only when a request is refused (getSpecPaths()), and reuses the result until the cached text changes.
  • Which connection it is: ctx.auth from workers-oauth-provider now reaches the tools as a Connection. A token the provider issued carries clientId and scope (oauth). A credential resolved by resolveExternalToken carries neither (direct).
  • execute: GlobalOutbound looks the permissions up only for a 401 or 403 and returns the explanation in X-Cloudflare-MCP-Refusal-Hint. The sandbox adds it to the thrown error, along with the method, path, status and documentation_url. That way it survives code that catches the error.
  • Endpoint tools: errors now start with Cloudflare API error: GET /path returned HTTP 403. The explanation follows the response body.

Next in the stack: #268, 403 insufficient_scope challenges, so clients that support step-up can ask for the missing scope themselves.

Ported onto the Forge artifacts: the build keeps x-api-token-group in
spec.json, endpoint tools use the permissions in their mcp-tools.json
entry, and execute parses spec.json paths only for a refused request.
mattzcarey added a commit that referenced this pull request Oct 9, 2026
Combines #267 and #268, ported from the Forge stack onto main.

- 401/403 tool errors name the method, path and status, and say whether the
  connection lacks a scope, already holds one, or needs an API token.
- The scheduled handler keeps x-api-token-group in spec.json and copies it
  into each mcp-tools.json entry as `permissions`.
- execute looks a refused path up in the cached mcp-tools.json, not the
  ~25 MB spec.json.
- An OAuth connection missing the scope gets a 403 insufficient_scope
  challenge when retrying the call is safe; ?scopeChallenge=tool keeps the
  challenge in the tool result's _meta instead.
mattzcarey added a commit that referenced this pull request Oct 12, 2026
Combines #267 and #268, ported from the Forge stack onto main.

- 401/403 tool errors name the method, path and status, and say whether the
  connection lacks a scope, already holds one, or needs an API token.
- The scheduled handler keeps x-api-token-group in spec.json and copies it
  into each mcp-tools.json entry as `permissions`.
- execute looks a refused path up in the cached mcp-tools.json, not the
  ~25 MB spec.json.
- An OAuth connection missing the scope gets a 403 insufficient_scope
  challenge when retrying the call is safe; ?scopeChallenge=tool keeps the
  challenge in the tool result's _meta instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant