Repository navigation
MCP OAuth tool refresh hides reauthorization and re-registers DCR client #1816
Description
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:
- Complete the PostHog OAuth connection successfully.
- Have the upstream endpoint reject the saved bearer while Executor still considers it unexpired.
- Refresh the connection's tools.
- 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: () => undefinedThat 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:
- completes an initial DCR OAuth connection,
- rejects the saved bearer during a later MCP
initialize, - 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.
Activity
ramarivera commentedon Aug 28, 2026
Live self-hosted reproduction after completing PostHog OAuth reconnection:
expiredtohealthybefore any upstream probe.dcr-oauth-posthog-com.billing:read,canvas:read,canvas:write,comment:write,dataset:read,dataset:write, allerror_tracking:*scopes, allfeature_flag:*scopes,insight:write,integration:write,llm_analytics:write,organization:write,project:write,query:write, andsession_recording:write.tools/listrequest returned HTTP 401. Executor then classified the connection as expired and kepttoolCount: 0.This isolates a second bug adjacent to the 401 classification fixed in #1818: Reconnect reuses the existing DCR client and reruns authorization, but it does not re-register the client when the protected resource's supported scopes have expanded. The authorization server can therefore issue a fresh token constrained to the old client registration, and repeated reconnects cannot repair it.
Current manual recovery sequence is:
Auto-registered clientssection.The durable product fix should detect DCR scope drift and replace or update the auto-registered client before starting authorization, rather than asking the user to reconnect indefinitely.
🤖 This content was generated with AI assistance using GPT-5.6 Sol.
ramarivera commentedon Aug 28, 2026
Correction after tracing the live UI and SDK paths: the manual recovery sequence is not fully reachable from the current UI for an integration whose automatic DCR path remains active.
AddAccountModalhides the OAuth app picker and itsAuto-registered clientsmanagement section whiledcrActiveis true (packages/react/src/components/add-account-modal.tsx:2507, management section at:2743).Add connectiondoes not replace the stale registration.registerDynamicClientcallsdecideDcrClientReuse, then immediately returns the existing slug when issuer/resource/redirect match (packages/core/sdk/src/oauth-service.ts:1170-1172). It never compares the current discovered scopes with those used to register that client.The current recovery requires the approval-gated API/CLI to remove the affected connection, then remove
dcr-oauth-posthog-com, followed by a fresh Add connection/consent flow. The product fix also needs either scope-aware DCR reuse or a reachable “replace auto-registered client” UI action.🤖 This content was generated with AI assistance using GPT-5.6 Sol.
ramarivera commentedon Aug 28, 2026
Follow-up after deleting the stale DCR client and completing a fresh PostHog OAuth connection: PostHog now selects CIMD (
posthog-mcp-cimd), but the zero-tools failure persists.Fresh live evidence:
https://mcp.posthog.com/.well-known/oauth-protected-resource/mcp.oauthScopecontains exactly 100 scopes.property_definition:write; entry 100,query:read, is the first omitted scope.query:read, all session-recording scopes, survey scopes, user scopes, and warehouse scopes.MCP OAuth re-authorization required, but the integration view suppresses that actionable state and simply exposes zero tools.This corrects the prior DCR-only hypothesis: deleting the stale DCR registration changes the registration protocol but does not resolve the live PostHog failure. The current token truncation happens upstream after Executor requests the full scope set. PostHog's current source resolves 144 MCP scopes, but its production authorization flow grants a deterministic 100-scope prefix.
PostHog upstream evidence: PostHog/posthog#55307 (comment)
Executor still has a product bug independently of PostHog: an upstream 401 requiring reauthorization must not silently render as a healthy-looking integration with zero tools. #1818 addresses that classification and UI handoff.
🤖 This content was generated with AI assistance using GPT-5.6 Sol.