Guided-auth removal, OAuth smoke testing, and TUI/CLI support (#1531)

* refactor(web): update OAuth E2E tests to use new authentication method

- Replaced `runGuidedAuth` with `authenticate` in the InspectorClient OAuth end-to-end tests to support quick authentication with CIMD pre-registration.
- Updated comments to reflect the change in authentication method and its support for HTTP test metadata URLs.
- Ensured that the tests continue to validate the expected authorization URL after the change.

Additionally, imported `ensureCimdClientRegistration` in the OAuthManager to streamline client registration during the authentication process.

* Guided-auth removal, OAuth smoke testing, and TUI/CLI support

* Added CLI OAuth runner flags tests

* Added clarifying comment on port 0 binding

* README notes on TUI oauth callback port, prettier fixes for CLI tests

* CIMD validation uses inline like EMA IdP

* Fixed TUI test failures from merge

* test: bring 5 OAuth files over the 90 per-file coverage gate

The guided-auth-removal work added/touched several files that fell below
the repo's >=90 per-file coverage gate (npm run coverage). CI doesn't run
the gate (it's local-only), so validate + test:integration passed while
these gaps slipped through. Bring them all back over the line:

- core/auth/node/runner-oauth-callback.ts: test bad-URL/non-http-scheme
  throws, no-port->80 default, and IPv6 hostname bracketing. Annotate the
  provably-dead guards (empty host, out-of-range port, String(err)
  fallback, pathname "/" fallback) -- all rejected by new URL() upstream --
  with justified v8 ignore comments instead of lowering the gate.
- core/auth/cimd.ts: test the AS-doesn't-support-CIMD early return.
- core/react/useClientSettingsDraft.ts: test the functional-updater path,
  the stale-onChange-after-close prev===null guard, and the
  timer-fires-after-close value===null guard.
- ClientSettingsForm.tsx: test the CIMD checkbox toggle and CIMD URL clear.
- clients/tui/src/utils/oauthDisplay.ts: test static/dcr registration
  kinds, the expired IdP session, and the configured-scope/empty-scope
  branches of formatScopes.

npm run coverage now passes (exit 0) across web, cli, tui, launcher.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: cliffhall <cliff@futurescale.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bob Dickinson 2026-06-29 12:35:51 -07:00 • committed by GitHub
commit 13dafe457b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -57,7 +57,10 @@ import {
serializeMcpConfig,
} from "@inspector/core/mcp/serverList.js";
import type { ClientConfig } from "@inspector/core/client/types.js";
import { getActiveEnterpriseManagedAuthIdp } from "@inspector/core/client/types.js";
import {
getActiveCimdClientMetadataUrl,
getActiveEnterpriseManagedAuthIdp,
} from "@inspector/core/client/types.js";
import { isEmaClientNotConfiguredError } from "@inspector/core/auth/ema/clientConfigError.js";
import {
loadClientConfigRemote,
@ -146,6 +149,7 @@ import {
OAUTH_PENDING_SERVER_KEY,
isUnauthorizedError,
} from "./utils/oauthFlow";
import { clearServerOAuthState } from "./utils/clearServerOAuthState";
// OAuth redirect URL provider — points at the dev backend's `/oauth/callback`
// handler. The InspectorClient only consults this when the active server
@ -1224,6 +1228,12 @@ function App() {
};
}, [connectionStatus, inspectorClient]);
const connectionInfoCanClearOAuth =
connectionStatus === "connected" &&
!!inspectorClient &&
(connectionInfoTransport === "streamable-http" ||
connectionInfoTransport === "sse");
// Derive log entries from the message log. Filters for
// `notifications/message` (the response to `logging/setLevel`).
const logs = useMemo<LogEntryData[]>(
@ -1315,6 +1325,7 @@ function App() {
// from the InspectorClient options we're about to derive from it.
const savedSettings = server.settings;
const activeIdp = getActiveEnterpriseManagedAuthIdp(clientConfig);
const activeCimdUrl = getActiveCimdClientMetadataUrl(clientConfig);
// Flatten the persisted settings into the InspectorClient options shape.
// Empty / zero values stay unset so the SDK defaults apply.
const defaultMetadata = savedSettings?.metadata
@ -1324,7 +1335,7 @@ function App() {
.map((m) => [m.key, m.value]),
)
: undefined;
const oauth =
const oauthFromServer =
savedSettings &&
(savedSettings.oauthClientId ||
savedSettings.oauthClientSecret ||
@ -1345,6 +1356,13 @@ function App() {
}),
}
: undefined;
const oauth =
oauthFromServer || activeCimdUrl
? {
...(oauthFromServer ?? {}),
...(activeCimdUrl && { clientMetadataUrl: activeCimdUrl }),
}
: undefined;
const client = new InspectorClient(server.config, {
environment,
// The Tasks tab needs the receiver-task pipeline; the
@ -1450,7 +1468,7 @@ function App() {
oauthCallbackHandledRef.current = true;
const params = parseOAuthCallbackParams(window.location.search);
// The OAuth `state` round-trips `{execution}:{authId}`; the authId is the
// The OAuth `state` round-trips the auth session id; the authId is the
// session key the pre-redirect page saved the fetch log under, so the
// rebuilt client can restore those `auth` entries. Read it before the
// URL is cleared below.
@ -2390,6 +2408,42 @@ function App() {
// target isn't resolvable.
const settingsModalIsStdio = settingsModalServerType === "stdio";
const clearServerOAuthAndDisconnect = useCallback(
async (server: { id: string; name: string; config: MCPServerConfig }) => {
const isActive = server.id === activeServerId;
const cleared = clearServerOAuthState({
config: server.config,
inspectorClient: isActive ? inspectorClient : null,
isActiveConnection: isActive,
});
if (!cleared) return;
if (isActive && inspectorClient) {
await inspectorClient.disconnect();
setConnectionInfoOAuthWhenConnected(undefined);
}
notifications.show({
title: "OAuth state cleared",
message: isActive
? "Stored tokens and client registration were removed. Reconnect to run a fresh authorization flow."
: `Stored OAuth state was removed for "${server.name}". Connect to authorize again.`,
color: "blue",
});
},
[activeServerId, inspectorClient],
);
const handleClearConnectionOAuth = useCallback(() => {
if (!activeServer) return;
void clearServerOAuthAndDisconnect(activeServer);
}, [activeServer, clearServerOAuthAndDisconnect]);
const handleClearStoredOAuthFromSettings = useCallback(() => {
if (!settingsModalTarget) return;
void clearServerOAuthAndDisconnect(settingsModalTarget);
}, [settingsModalTarget, clearServerOAuthAndDisconnect]);
const onSettingsModalClose = useCallback(() => {
flushSettingsDraft();
// Apply root edits to the live client once, on close — not on every
@ -2689,6 +2743,9 @@ function App() {
isStdio={settingsModalIsStdio}
onClose={onSettingsModalClose}
onSettingsChange={onSettingsChange}
onClearStoredOAuth={
settingsModalIsStdio ? undefined : handleClearStoredOAuthFromSettings
}
/>
<ClientSettingsModal
key={
@ -2709,6 +2766,9 @@ function App() {
clientCapabilities={clientCapabilities}
transport={connectionInfoTransport}
oauth={connectionInfoOAuth}
onClearOAuth={
connectionInfoCanClearOAuth ? handleClearConnectionOAuth : undefined
}
/>
)}
<ServerRemoveConfirmModal