different-ai/openwork · error · ProbeFailure

PROVIDER_AUTHORIZATION

PROVIDER_AUTHORIZATION

Error message

${message} (HTTP 403)

What it means

Thrown when a tools/list request is answered with HTTP 403, wrapping the server's error message (read via safeHttpErrorMessage) as `${message} (HTTP 403)` under the PROVIDER_AUTHORIZATION code with hint provider_per_user_403. This distinguishes per-user authorization failures during tool discovery from protocol-level discovery errors.

Source

Thrown at packages/enterprise-mcp-mock-server/src/testing/probe.ts:811

    startedAt = Date.now()
    const toolNames = new Set<string>()
    const discoveredTools: MockTool[] = []
    const cursors = new Set<string>()
    let cursor: string | undefined
    let catalogComplete = false
    for (let page = 0; page < 25; page += 1) {
      if (cursor) {
        if (cursors.has(cursor)) throw new ProbeFailure("MCP_TOOL_DISCOVERY", "mcp_pagination_loop", "Tool catalog repeated a cursor")
        cursors.add(cursor)
      }
      const listRawResponse = await fetchStep(mcpUrl, {
          method: "POST",
          headers: sessionHeaders,
          body: JSON.stringify({ jsonrpc: "2.0", id: 10 + page, method: "tools/list", params: cursor ? { cursor } : {} }),
        }, "MCP_TOOL_DISCOVERY", overallDeadline)
      if (listRawResponse.status === 403) {
        const message = await safeHttpErrorMessage(listRawResponse, "PROVIDER_AUTHORIZATION", "provider_per_user_403")
        throw new ProbeFailure("PROVIDER_AUTHORIZATION", "provider_per_user_403", `${message} (HTTP 403)`)
      }
      const listResponse = await expectOk(listRawResponse, "MCP_TOOL_DISCOVERY")
      const envelope = await parseRpc(listResponse, "MCP_TOOL_DISCOVERY")
      if (envelope.id !== 10 + page) {
        throw new ProbeFailure("MCP_TOOL_DISCOVERY", "mcp_tools_discovery", "tools/list response JSON-RPC id did not match the request")
      }
      if (envelope.error) throw new ProbeFailure("MCP_TOOL_DISCOVERY", "mcp_tools_discovery", envelope.error.message)
      const result = parseAt(
        toolsListResultSchema,
        envelope.result,
        "MCP_TOOL_DISCOVERY",
        "mcp_tools_discovery",
        "tools/list result did not match the required shape",
      )
      if (result.nextCursor && cursors.has(result.nextCursor)) {
        throw new ProbeFailure("MCP_TOOL_DISCOVERY", "mcp_pagination_loop", "Tool catalog repeated a cursor")
      }
      for (const rawTool of result.tools) {

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Grant the probe's identity the scopes/entitlements needed for tools/list
  2. Regenerate the synthetic access token with the correct scopes and audience
  3. Verify server-side authorization policy for per-user tool access
  4. Check token audience/resource claims match what the MCP resource expects

Example fix

// before
token = issueToken({ scopes: ["mcp:read"] })
// after
token = issueToken({ scopes: ["mcp:read", "mcp:tools:list"] })
Defensive patterns

Strategy: try-catch

Validate before calling

const requiredScopes = ["mcp:tools:list"]
const missing = requiredScopes.filter((s) => !tokenScopes.includes(s))
if (missing.length) throw new Error(`token missing scopes: ${missing.join(",")}`)

Type guard

null

Try / catch

try {
  await probeEnterpriseMcpMockServer(scenario)
} catch (e) {
  if (e instanceof ProbeFailure && e.code === "PROVIDER_AUTHORIZATION") {
    console.error("Per-user authorization denied (403):", e.message)
  }
}

Prevention

When it happens

Trigger: The synthetic access token used by the probe lacks the scope/entitlement required to list tools on the target MCP resource, and the server responds 403 to the tools/list POST.

Common situations: OAuth token issued without the required tool-catalog scope; per-user entitlement not granted to the probe's test account; admin disabled tool access for the user; wrong audience/resource indicator so authorization falls through to a blanket denial.

Understand the failure class

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/1441dc27d61a2dca. Report an issue: GitHub.