Skip to content

MCP OAuth tool refresh hides reauthorization and re-registers DCR client #1816

Description

@ramarivera

Bug description

A saved OAuth-backed remote MCP connection can be rejected by the upstream MCP endpoint before its locally stored expiry. During remote tool catalog refresh, Executor turns the resulting McpOAuthReauthorizationRequired into { tools: [], incomplete: true }.

The deployed 1.6.0 core then preserves the previous catalog and logs only plugin returned an incomplete tool catalog. Current main improved the detail in #1563 by carrying incompleteReason, but it still classifies the connection as degraded, not expired. When the connection has never produced tools, users still see zero tools without the reconnect state added by #1316.

The MCP SDK also performs Dynamic Client Registration before requesting an authorization redirect because the plugin's OAuthClientProvider returns no saved client information. A noninteractive catalog refresh cannot complete that redirect, so the refresh can register a fresh OAuth client and abandon it.

PR #1316 fixed the adjacent human reconnect handoff and expired health rendering. Current main still catches this typed failure in packages/plugins/mcp/src/sdk/plugin.ts inside resolveTools and returns it as an incomplete catalog with degraded health.

Reproduction

Observed on self-host 1.6.0 with a saved PostHog MCP OAuth connection:

  1. Complete the PostHog OAuth connection successfully.
  2. Have the upstream endpoint reject the saved bearer while Executor still considers it unexpired.
  3. Refresh the connection's tools.
  4. Executor returns an empty catalog with degraded health and logs plugin returned an incomplete tool catalog.

Temporary instrumentation around the real Executor 1.6.0 MCP transport produced this sanitized sequence:

POST https://mcp.posthog.com/mcp -> 401
GET  https://mcp.posthog.com/.well-known/oauth-protected-resource/mcp -> 200
GET  https://oauth.posthog.com/.well-known/oauth-authorization-server -> 200
POST https://oauth.posthog.com/oauth/register/ -> 201
McpToolDiscoveryError: Failed connecting to MCP server: MCP OAuth re-authorization required

The initial 401 had no WWW-Authenticate header and no insufficient_scope indication. Executor never sent tools/list.

Current code path

discoverTools preserves the connector failure as McpToolDiscoveryError. On current main, mcpPlugin().resolveTools catches every discovery error and returns an incomplete result with a human-readable reason:

{
  tools: [],
  incomplete: true,
  incompleteReason: discovered.failure.message,
}

Core's tool-sync health path maps every incomplete result to status: "degraded", so the typed reconnect condition is still lost even though its message now survives.

The plugin's makeOAuthProvider also implements:

clientInformation: () => undefined
saveClientInformation: () => undefined

That makes the SDK dynamically register again after the saved bearer is rejected, even though the saved connection already references an OAuth client.

Expected behavior

  • Preserve a typed reauthorization-required outcome from MCP discovery instead of collapsing it into an anonymous incomplete catalog.
  • Preserve any previously persisted tool catalog.
  • Persist an expired or reconnect-required health result on the saved connection so the existing reconnect UI from Return expired health verdicts for rejected OAuth refreshes and fix DCR reconnect #1316 can act on it.
  • Do not dynamically register a new OAuth client during background refresh of an existing saved OAuth connection.
  • Do not expose bearer tokens, OAuth client secrets, or raw upstream response bodies in logs or API errors.
  • Keep ordinary network/schema discovery failures on the existing catalog-preservation path.

Suggested regression coverage

Use the repository OAuth/MCP emulators with a server that:

  1. completes an initial DCR OAuth connection,
  2. rejects the saved bearer during a later MCP initialize,
  3. records all OAuth and MCP requests.

Assert that refresh:

  • preserves the existing tools,
  • marks the connection expired or reconnect-required,
  • performs no tools/list,
  • performs no second /register,
  • returns an actionable typed result rather than an internal error or anonymous incomplete-catalog reason.

Environment

  • Executor self-host image: 1.6.0
  • Host: Linux amd64, Docker self-host
  • Remote transport: streamable HTTP
  • Upstream MCP: PostHog

🤖 This content was generated with AI assistance using GPT-5.6 Sol.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions