chore: close quality-gate coverage gaps (#1689) (#1766)
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled

Closes #1689. Typecheck cli/tui src, add a core/ format+lint gate, and gate core/json + core/client coverage. See PR #1766 for the full review history.
This commit is contained in:
Cliff Hall 2026-07-24 17:20:12 -04:00 • committed by GitHub
commit 373f346e5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 2210 additions and 467 deletions

View file

@ -65,11 +65,7 @@ export class AuthChallengeError extends Error {
readonly authChallenge: AuthChallenge;
readonly status: number;
constructor(
authChallenge: AuthChallenge,
status: number,
message?: string,
) {
constructor(authChallenge: AuthChallenge, status: number, message?: string) {
super(message ?? `Auth challenge: ${authChallenge.reason}`);
this.name = "AuthChallengeError";
this.authChallenge = authChallenge;
@ -262,8 +258,7 @@ export function parseAuthChallengeFromError(
}
const status =
(err as { status?: number }).status ??
(err as { code?: number }).code;
(err as { status?: number }).status ?? (err as { code?: number }).code;
if (status !== 401 && status !== 403) {
return undefined;
}
@ -271,8 +266,9 @@ export function parseAuthChallengeFromError(
const wwwAuthenticate =
authChallenge?.raw?.wwwAuthenticate ??
(err as { wwwAuthenticate?: string }).wwwAuthenticate ??
(err as { headers?: { get?: (name: string) => string | null } }).headers
?.get?.("WWW-Authenticate") ??
(
err as { headers?: { get?: (name: string) => string | null } }
).headers?.get?.("WWW-Authenticate") ??
undefined;
if (!wwwAuthenticate?.length) {
@ -309,7 +305,8 @@ export function isAuthChallengeError(err: unknown): boolean {
return false;
}
const authChallenge = (err as { authChallenge?: AuthChallenge }).authChallenge;
const authChallenge = (err as { authChallenge?: AuthChallenge })
.authChallenge;
if (authChallenge?.reason) {
return true;
}
@ -323,8 +320,9 @@ export function isAuthChallengeError(err: unknown): boolean {
const wwwAuthenticate =
authChallenge?.raw?.wwwAuthenticate ??
(err as { wwwAuthenticate?: string }).wwwAuthenticate ??
(err as { headers?: { get?: (name: string) => string | null } }).headers
?.get?.("WWW-Authenticate") ??
(
err as { headers?: { get?: (name: string) => string | null } }
).headers?.get?.("WWW-Authenticate") ??
undefined;
return wwwAuthenticate !== undefined && wwwAuthenticate.length > 0;

View file

@ -22,7 +22,10 @@ export {
normalizeIdpIssuer,
type EmaIdpLoginState,
} from "./idpSession.js";
export { discoverEmaResourceContext, resolveEmaScopes } from "./resourceContext.js";
export {
discoverEmaResourceContext,
resolveEmaScopes,
} from "./resourceContext.js";
export {
EmaClientNotConfiguredError,
emaClientNotConfiguredMessage,

View file

@ -25,13 +25,14 @@ export type { BuildOAuthConnectionStateParams } from "./connection-state.js";
export { ensureCimdClientRegistration } from "./cimd.js";
export { mcpAuth, type McpAuthOptions, type McpAuthResult } from "./mcpAuth.js";
export {
computeScopeUnion,
isStrictScopeSuperset,
} from "./scopes.js";
export { computeScopeUnion, isStrictScopeSuperset } from "./scopes.js";
// Storage
export type { OAuthStorage, IdpSessionState, SaveClientInformationOptions } from "./storage.js";
export type {
OAuthStorage,
IdpSessionState,
SaveClientInformationOptions,
} from "./storage.js";
export { getServerSpecificKey, OAUTH_STORAGE_KEYS } from "./storage.js";
// Providers

View file

@ -1,7 +1,4 @@
export {
NodeOAuthStorage,
clearAllOAuthClientState,
} from "./storage-node.js";
export { NodeOAuthStorage, clearAllOAuthClientState } from "./storage-node.js";
export {
createOAuthCallbackServer,
OAuthCallbackServer,
@ -21,9 +18,7 @@ export {
parseRunnerOAuthCallbackUrl,
} from "./runner-oauth-callback.js";
export type { RunnerOAuthCallbackConfig } from "./runner-oauth-callback.js";
export {
runRunnerInteractiveOAuth,
} from "./runner-interactive-oauth.js";
export { runRunnerInteractiveOAuth } from "./runner-interactive-oauth.js";
export type {
RunRunnerInteractiveOAuthOptions,
RunnerInteractiveOAuthClient,

View file

@ -32,9 +32,7 @@ export function parseRunnerOAuthCallbackUrl(
cliCallbackUrl?: string,
): RunnerOAuthCallbackConfig {
const raw =
cliCallbackUrl?.trim() ||
process.env.MCP_OAUTH_CALLBACK_URL?.trim() ||
"";
cliCallbackUrl?.trim() || process.env.MCP_OAUTH_CALLBACK_URL?.trim() || "";
if (!raw) {
return {
hostname: RUNNER_OAUTH_CALLBACK_DEFAULT_HOSTNAME,

View file

@ -13,10 +13,7 @@
* browser side never imports this; it gets values rehydrated into the
* `/api/servers` response by the Hono handler.
*/
import {
AsyncEntry,
findCredentialsAsync,
} from "@napi-rs/keyring";
import { AsyncEntry, findCredentialsAsync } from "@napi-rs/keyring";
const SERVICE_NAME = "mcp-inspector";

View file

@ -84,8 +84,7 @@ export function isStepUpConfirmation(
options?: { enterpriseManaged?: boolean },
): boolean {
return (
isStandardOAuthStepUp(challenge, options) ||
isEmaStepUp(challenge, options)
isStandardOAuthStepUp(challenge, options) || isEmaStepUp(challenge, options)
);
}
@ -124,9 +123,7 @@ export function stepUpFollowUpMessage(options?: {
export function stepUpAuthorizeActionLabel(options?: {
enterpriseManaged?: boolean;
}): string {
return options?.enterpriseManaged
? "Authorize"
: "Authorize (opens browser)";
return options?.enterpriseManaged ? "Authorize" : "Authorize (opens browser)";
}
export function stepUpModalTitle(options?: {
@ -198,7 +195,9 @@ export function oauthPreRedirectToastCopy(
const name = options.serverName;
if (authKind === "step_up") {
return {
title: name ? `Step-up authorization for "${name}"` : "Step-up authorization",
title: name
? `Step-up authorization for "${name}"`
: "Step-up authorization",
message: "Redirecting to authorize additional permissions…",
};
}

View file

@ -71,7 +71,9 @@ export const CIMD_METADATA_URL_PATH_ERROR =
* Returns undefined when the value is valid; empty strings are not flagged here
* (required-field gating lives in {@link canPersistClientSettingsDraft}).
*/
export function getCimdClientMetadataUrlError(value: string): string | undefined {
export function getCimdClientMetadataUrlError(
value: string,
): string | undefined {
const trimmed = value.trim();
if (trimmed === "") return undefined;
if (!isAbsoluteHttpUrl(trimmed)) {

View file

@ -2,10 +2,7 @@
* Remote HTTP load/save for client.json via /api/storage/client.
*/
import {
parseClientConfig,
serializeClientConfig,
} from "./config-parse.js";
import { parseClientConfig, serializeClientConfig } from "./config-parse.js";
import type { ClientConfig } from "./types.js";
export interface RemoteClientConfigOptions {

View file

@ -3,8 +3,14 @@
* for Node runners (TUI, CLI).
*/
import { KeyringSecretStore } from "../auth/node/secret-store.js";
import type { InspectorClientOptions, InspectorServerSettings } from "../mcp/types.js";
import {
KeyringSecretStore,
type SecretStore,
} from "../auth/node/secret-store.js";
import type {
InspectorClientOptions,
InspectorServerSettings,
} from "../mcp/types.js";
import { loadClientConfig } from "./config.js";
import type { ClientConfig } from "./types.js";
import {
@ -15,6 +21,9 @@ import {
export interface LoadRunnerClientConfigOptions {
/** Explicit path from `--client-config` (or MCP_CLIENT_CONFIG_PATH when unset). */
clientConfigPath?: string;
/** Secret store for the IdP clientSecret; defaults to the OS keychain. Tests
* inject an in-memory store for determinism. */
secretStore?: SecretStore;
}
/** Load install-level client.json with keychain-backed IdP secrets. */
@ -25,7 +34,7 @@ export async function loadRunnerClientConfig(
options?.clientConfigPath?.trim() ||
process.env.MCP_CLIENT_CONFIG_PATH?.trim() ||
undefined;
const secretStore = new KeyringSecretStore();
const secretStore = options?.secretStore ?? new KeyringSecretStore();
return loadClientConfig({ filePath: customPath, secretStore });
}
@ -53,7 +62,10 @@ export function buildRunnerClientAuthOptions(
cliOverrides?: RunnerClientConfigOverrides,
): Pick<
InspectorClientOptions,
"oauth" | "enterpriseManagedAuth" | "installEnterpriseManagedAuth" | "directAuthRecovery"
| "oauth"
| "enterpriseManagedAuth"
| "installEnterpriseManagedAuth"
| "directAuthRecovery"
> {
const activeIdp = getActiveEnterpriseManagedAuthIdp(clientConfig);
const activeCimdUrl = getActiveCimdClientMetadataUrl(clientConfig);
@ -84,9 +96,7 @@ export function buildRunnerClientAuthOptions(
cliOverrides?.clientMetadataUrl?.trim() || activeCimdUrl;
const oauthFromCli =
cliOverrides?.clientId ||
cliOverrides?.clientSecret ||
clientMetadataUrl
cliOverrides?.clientId || cliOverrides?.clientSecret || clientMetadataUrl
? {
...(cliOverrides?.clientId && { clientId: cliOverrides.clientId }),
...(cliOverrides?.clientSecret && {

View file

@ -28,9 +28,7 @@ export interface ClientConfig {
}
/** True when install-level EMA IdP config is active (not just stored). */
export function isEnterpriseManagedAuthEnabled(
config: ClientConfig,
): boolean {
export function isEnterpriseManagedAuthEnabled(config: ClientConfig): boolean {
const ema = config.enterpriseManagedAuth;
if (!ema?.idp) return false;
return ema.enabled !== false;

View file

@ -32,9 +32,7 @@ export function isOAuthCapableServerType(type: ServerType): boolean {
* MCP server URL used as the OAuth storage key (includes path, for discovery).
* Undefined for stdio transports.
*/
export function getOAuthServerUrl(
config: MCPServerConfig,
): string | undefined {
export function getOAuthServerUrl(config: MCPServerConfig): string | undefined {
if (config.type === "sse" || config.type === "streamable-http") {
return config.url;
}

View file

@ -1827,9 +1827,12 @@ export class InspectorClient extends InspectorClientEventTarget {
// a future modern-family revision would negotiate a different string, and
// the two must not disagree. The raw channel only runs on a connected modern
// session, so this is always set; the constant is a defensive fallback.
/* v8 ignore next -- fallback only if getProtocolVersion() is unset, which
can't happen on the connected modern session this runs on. */
const protocolVersion = this.getProtocolVersion() ?? MODERN_PROTOCOL_VERSION;
/* v8 ignore start -- fallback only if getProtocolVersion() is unset, which
can't happen on the connected modern session this runs on. Bracketed so
the ignore is reflow-proof however prettier splits the statement. */
const protocolVersion =
this.getProtocolVersion() ?? MODERN_PROTOCOL_VERSION;
/* v8 ignore stop */
return {
...params,
_meta: {
@ -1861,9 +1864,7 @@ export class InspectorClient extends InspectorClientEventTarget {
...message,
result: {
resultType: "complete",
content: [
{ type: "text", text: `Modern task ${task.taskId} created` },
],
content: [{ type: "text", text: `Modern task ${task.taskId} created` }],
_meta: { [MODERN_TASK_HANDLE_META]: task },
},
};
@ -1906,7 +1907,9 @@ export class InspectorClient extends InspectorClientEventTarget {
const raw = await new Promise<unknown>((resolve, reject) => {
const timer = setTimeout(() => {
this.pendingRawWireRequests.delete(id);
reject(new Error(`Raw request "${method}" timed out after ${timeoutMs} ms`));
reject(
new Error(`Raw request "${method}" timed out after ${timeoutMs} ms`),
);
}, timeoutMs);
this.pendingRawWireRequests.set(id, { resolve, reject, timer });
transport.send(message).catch((err: unknown) => {
@ -4443,7 +4446,10 @@ export class InspectorClient extends InspectorClientEventTarget {
this.modernReconnectTimer = undefined;
// Disconnect/unsubscribe may have raced the timer — bail if the reconnect
// is no longer wanted.
if (isTerminalStatus(this.status) || this.subscribedResources.size === 0) {
if (
isTerminalStatus(this.status) ||
this.subscribedResources.size === 0
) {
return;
}
this.refreshModernSubscription(true).catch(() =>

View file

@ -32,4 +32,4 @@ export function createAuthChallengeInterceptFetch(
`MCP auth challenge (${response.status})`,
);
};
}
}

View file

@ -1,8 +1,5 @@
import type { SecretStore } from "../../auth/node/secret-store.js";
import {
expectedSecretFields,
mergeSecretsIntoStored,
} from "../serverList.js";
import { expectedSecretFields, mergeSecretsIntoStored } from "../serverList.js";
import type { MCPConfig } from "../types.js";
/**

View file

@ -72,7 +72,10 @@ export class ManagedRequestorTasksState extends TypedEventTarget<ManagedRequesto
// A status update is ignored when the task was dismissed (clearCompleted)
// or when the user cancelled it and the update isn't itself "cancelled" —
// the deliberate cancel wins over a late completed/working update.
const shouldIgnoreUpdate = (taskId: string, status: Task["status"]): boolean =>
const shouldIgnoreUpdate = (
taskId: string,
status: Task["status"],
): boolean =>
this.dismissedTaskIds.has(taskId) ||
(this.cancelledTaskIds.has(taskId) && status !== "cancelled");
const onTaskStatusChange = (
@ -200,7 +203,9 @@ export class ManagedRequestorTasksState extends TypedEventTarget<ManagedRequesto
* whole refresh. No known tasks ⇒ a no-op refresh (the correct "no server
* list" semantics).
*/
private async refreshModern(client: InspectorClientProtocol): Promise<Task[]> {
private async refreshModern(
client: InspectorClientProtocol,
): Promise<Task[]> {
const ids = this.tasks
.map((t) => t.taskId)
.filter((id) => !this.dismissedTaskIds.has(id));

View file

@ -25,7 +25,10 @@ import type {
InspectorResourceSubscription,
ResourceSubscriptionStreamState,
} from "../types.js";
import { isTerminalStatus, INACTIVE_SUBSCRIPTION_STREAM_STATE } from "../types.js";
import {
isTerminalStatus,
INACTIVE_SUBSCRIPTION_STREAM_STATE,
} from "../types.js";
import type { Resource } from "@modelcontextprotocol/client";
import {
TypedEventTarget,

View file

@ -583,8 +583,7 @@ export const MODERN_LOG_LEVELS: ModernLogLevel[] = [
/** Runtime guard for the {@link ModernLogLevel} literal (hand-edited files). */
export function isModernLogLevel(value: unknown): value is ModernLogLevel {
return (
typeof value === "string" &&
(MODERN_LOG_LEVELS as string[]).includes(value)
typeof value === "string" && (MODERN_LOG_LEVELS as string[]).includes(value)
);
}

View file

@ -74,9 +74,7 @@ export function useClientSettingsDraft<T>({
const prev = latestValuesRef.current;
if (prev === null) return;
const resolved =
typeof next === "function"
? (next as (prev: T) => T)(prev)
: next;
typeof next === "function" ? (next as (prev: T) => T)(prev) : next;
latestValuesRef.current = resolved;
setDraft(resolved);
if (timerRef.current) clearTimeout(timerRef.current);