You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A CIMD client_id is a URL on the deployment itself that the provider's authorization server must fetch. A self-host that is not publicly reachable (LAN, VPN, tailnet) can never serve that fetch, and since #1732 the automatic connect flow prefers CIMD with no fallback, so connects to providers that advertise it (Granola, Linear, Cloudflare) dead-end on invalid_client. This adds EXECUTOR_OAUTH_DISABLE_CIMD=true for the self-host: it suppresses CIMD support in the server-side oauth.probe result, so automatic connects fall through to dynamic client registration (or manual setup). Filtering at the probe needs no SPA changes, so it works with the prebuilt image, where the CIMD base URL is a build-time define.
bun run test (new: packages/core/sdk/src/oauth-probe-cimd.test.ts 2/2, apps/host-selfhost/src/executor-config.test.ts 24/24 including exact-string opt-out cases)
e2e - MCP OAuth · EXECUTOR_OAUTH_DISABLE_CIMD connects through dynamic registration (new scenario, boots its own instance with the flag): cd e2e && bunx vitest run --project selfhost selfhost/mcp-oauth-disable-cimd.test.ts. The connect completes healthy through exactly one POST /register, and the authorize request carries the minted client_... id, not a metadata-document URL.
Red-proof: the same scenario without the flag fails with expected [] to have a length of 1 (zero registration requests), and its trace shows client_id=http://localhost:<port>/api/oauth/client-id-metadata/default.json.
The existing mcp-oauth-cimd-connect scenario still passes, so default behavior is unchanged: the flag is opt-in and no host sets it unless the operator does.
Against real providers from a tailnet-only self-host: the reproduction transcripts in the linked issue show Granola rejecting the private CIMD client_id as invalid_client while accepting DCR from the same host.
Real provider check. The same build was pointed at Granola's live MCP (https://mcp.granola.ai/mcp) from a host whose origin the provider cannot fetch, through the actual Add integration -> Connect flow, once per configuration:
Without the flag, the popup dead-ends on mcp-auth.granola.ai/oauth2/error?error=application_not_found:
With EXECUTOR_OAUTH_DISABLE_CIMD=true, the popup reaches Granola's sign-in page with a live authorization session for the DCR-minted client and the host's own callback as the redirect_uri:
Checklist
Added a changeset (bun run changeset), or this change needs none.
Added or updated tests for the new behaviour.
No secrets, credentials, or private data in the diff.
Choices worth a look
An explicit opt-in flag, not reachability auto-detection. The host cannot reliably know whether its web origin is fetchable from an arbitrary provider's network, and a probe-time reachability check would add latency and a new failure mode to every connect. The operator knows; one env var lets them say it.
Probe-side filtering, not client plumbing.VITE_EXECUTOR_CIMD_CLIENT_ID_METADATA_BASE_URL looks adjacent but does not solve this: it is a build-time Vite define (unavailable in the published image), and the hosted local.json it could point at declares loopback-only redirect_uris with application_type: "native", which can never match a self-host's HTTPS callback. Suppressing CIMD in the probe result changes only what automatic connects choose; discovery output is otherwise untouched and nothing server-side rejects an explicitly configured CIMD client.
Scope. This covers probe-driven automatic connects (MCP). OpenAPI integrations persist supportsClientIdMetadataDocument into their auth template at add-time and read it back without probing, so an OpenAPI integration added before the flag was set keeps its CIMD client; that path is out of scope here and called out in the issue.
CI note. The new scenario boots its own vite dev instance (the flag is a boot-time knob; setting it on the shared instance would invert what mcp-oauth-cimd-connect asserts underneath every other scenario). That adds one own-boot scenario to the selfhost-docker gate as well, same pattern as mcp-session-idle-eviction.
Verdict: mergeable. Open 2 days; no conflicts with current main (branch is one commit behind, applies cleanly).
Correctness. The mechanism is right and minimal: the only behavior change is the && in packages/core/sdk/src/oauth-service.ts probe result, and packages/react/src/components/add-account-modal.tsx (if (probe.clientIdMetadataDocumentSupported === true) → else DCR / no-registration-endpoint fallback) is the only consumer that branches on it, so flipping it at the probe is exactly how the SPA already picks CIMD vs DCR. No token persistence, credential storage, or connection-health code is touched; discovery output and explicitly configured CIMD clients are unaffected. Plumbing (HostConfigShape → ExecutorConfig → OAuthServiceDeps) follows the existing enterpriseManagedRollout/toolsSyncTtlMs pattern; env parsing mirrors EXECUTOR_ALLOW_LOCAL_NETWORK (exact "true"). Only selfhost wires it; cloud/CLI are unchanged, which is correct since the problem is specific to unreachable origins. The OpenAPI add-time supportsClientIdMetadataDocument gap is real but correctly scoped out in the description.
Ran locally on the PR branch (all pass):
bun run lint, bun run format:check, bun run typecheck
packages/core/sdk: vitest run src/oauth-probe-cimd.test.ts — 2/2
apps/host-selfhost: vitest run src/executor-config.test.ts — 24/24
e2e: vitest run --project selfhost selfhost/mcp-oauth-disable-cimd.test.ts selfhost/mcp-oauth-cimd-connect.test.ts — 2/2 (new scenario registers exactly one DCR client; existing CIMD scenario still passes, so the default is unchanged)
CI. The one red check, E2E (cloud 13of16), fails in cloud/mcp-session-cap-eviction.test.ts (openSession (session-5): no mcp-session-id header). This PR does not touch cloud or MCP session code, and the same job failed on the PR's base commit on main (e8590a0, run 33375809482); it passes on current main. A rebase should clear it.
Nothing pushed; no changes needed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A CIMD
client_idis a URL on the deployment itself that the provider's authorization server must fetch. A self-host that is not publicly reachable (LAN, VPN, tailnet) can never serve that fetch, and since #1732 the automatic connect flow prefers CIMD with no fallback, so connects to providers that advertise it (Granola, Linear, Cloudflare) dead-end oninvalid_client. This addsEXECUTOR_OAUTH_DISABLE_CIMD=truefor the self-host: it suppresses CIMD support in the server-sideoauth.proberesult, so automatic connects fall through to dynamic client registration (or manual setup). Filtering at the probe needs no SPA changes, so it works with the prebuilt image, where the CIMD base URL is a build-time define.Linked issue
Fixes #1910
Verification
bun run format:checkbun run lintbun run typecheckbun run test(new:packages/core/sdk/src/oauth-probe-cimd.test.ts2/2,apps/host-selfhost/src/executor-config.test.ts24/24 including exact-string opt-out cases)MCP OAuth · EXECUTOR_OAUTH_DISABLE_CIMD connects through dynamic registration(new scenario, boots its own instance with the flag):cd e2e && bunx vitest run --project selfhost selfhost/mcp-oauth-disable-cimd.test.ts. The connect completes healthy through exactly onePOST /register, and the authorize request carries the mintedclient_...id, not a metadata-document URL.expected [] to have a length of 1(zero registration requests), and its trace showsclient_id=http://localhost:<port>/api/oauth/client-id-metadata/default.json.mcp-oauth-cimd-connectscenario still passes, so default behavior is unchanged: the flag is opt-in and no host sets it unless the operator does.client_idasinvalid_clientwhile accepting DCR from the same host.Real provider check. The same build was pointed at Granola's live MCP (
https://mcp.granola.ai/mcp) from a host whose origin the provider cannot fetch, through the actual Add integration -> Connect flow, once per configuration:mcp-auth.granola.ai/oauth2/error?error=application_not_found:EXECUTOR_OAUTH_DISABLE_CIMD=true, the popup reaches Granola's sign-in page with a live authorization session for the DCR-minted client and the host's own callback as theredirect_uri:Checklist
bun run changeset), or this change needs none.Choices worth a look
An explicit opt-in flag, not reachability auto-detection. The host cannot reliably know whether its web origin is fetchable from an arbitrary provider's network, and a probe-time reachability check would add latency and a new failure mode to every connect. The operator knows; one env var lets them say it.
Probe-side filtering, not client plumbing.
VITE_EXECUTOR_CIMD_CLIENT_ID_METADATA_BASE_URLlooks adjacent but does not solve this: it is a build-time Vite define (unavailable in the published image), and the hostedlocal.jsonit could point at declares loopback-onlyredirect_uriswithapplication_type: "native", which can never match a self-host's HTTPS callback. Suppressing CIMD in the probe result changes only what automatic connects choose; discovery output is otherwise untouched and nothing server-side rejects an explicitly configured CIMD client.Scope. This covers probe-driven automatic connects (MCP). OpenAPI integrations persist
supportsClientIdMetadataDocumentinto their auth template at add-time and read it back without probing, so an OpenAPI integration added before the flag was set keeps its CIMD client; that path is out of scope here and called out in the issue.CI note. The new scenario boots its own vite dev instance (the flag is a boot-time knob; setting it on the shared instance would invert what
mcp-oauth-cimd-connectasserts underneath every other scenario). That adds one own-boot scenario to theselfhost-dockergate as well, same pattern asmcp-session-idle-eviction.