Real deployments gate IdP selection, prompt behavior, or audience on a query
parameter the OAuth/OIDC core specs never standardized (Keycloak's
`kc_idp_hint`, OIDC's `login_hint` / `prompt` / `acr_values`, Auth0's
`audience`), and none of them could be driven from the Inspector.
The Inspector never builds the authorize URL — the SDK's `auth()` does, and it
exposes no hook for extra parameters — so the merge happens at the one seam
that sees the finished URL: `BaseOAuthClientProvider.redirectToAuthorization`.
It lives in `core/`, so web, CLI, and TUI all inherit it.
- `core/auth/authorizationParams.ts`: the reserved-key list (`client_id`,
`code_challenge`, `code_challenge_method`, `redirect_uri`, `resource`,
`response_type`, `scope`, `state`), the key validator the form renders
inline, and the merge itself. Reserved keys are dropped with a warning
rather than silently overriding PKCE / the state binding / RFC 8707;
blank-key rows are skipped; the caller's URL is never mutated.
- Applied to the authorization request only — nothing wires it into the token
request.
- Persisted as `oauth.authorizationParams` (a string record) on the per-server
config beside `scopes`, edited as key/value rows in Server Settings ->
Authorization ("Additional authorization parameters"), with an inline error
per offending row plus a section-level warning.
- The enterprise-managed (EMA) leg redirects to a *different* authorization
server (the enterprise IdP), so it goes through the explicit
`redirectToExternalAuthorization` entry point, which skips the merge — a
`kc_idp_hint` or `audience` meant for the MCP server's AS must never be
appended to the IdP's authorize URL.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDXCSrnSBV7vZfxycG5Z6E
Signed-off-by: cliffhall <cliff@futurescale.com>
136 lines
4.2 KiB
TypeScript
136 lines
4.2 KiB
TypeScript
/**
|
|
* Install-level client config loading and InspectorClient auth option wiring
|
|
* for Node runners (TUI, CLI).
|
|
*/
|
|
|
|
import {
|
|
KeyringSecretStore,
|
|
type SecretStore,
|
|
} from "../auth/node/secret-store.js";
|
|
import type {
|
|
InspectorClientOptions,
|
|
InspectorServerSettings,
|
|
} from "../mcp/types.js";
|
|
import { oauthAuthorizationParamsFromSettings } from "../mcp/serverList.js";
|
|
import { loadClientConfig } from "./config.js";
|
|
import type { ClientConfig } from "./types.js";
|
|
import {
|
|
getActiveCimdClientMetadataUrl,
|
|
getActiveEnterpriseManagedAuthIdp,
|
|
} from "./types.js";
|
|
|
|
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. */
|
|
export async function loadRunnerClientConfig(
|
|
options?: LoadRunnerClientConfigOptions,
|
|
): Promise<ClientConfig> {
|
|
const customPath =
|
|
options?.clientConfigPath?.trim() ||
|
|
process.env.MCP_CLIENT_CONFIG_PATH?.trim() ||
|
|
undefined;
|
|
const secretStore = options?.secretStore ?? new KeyringSecretStore();
|
|
return loadClientConfig({ filePath: customPath, secretStore });
|
|
}
|
|
|
|
export interface RunnerClientConfigOverrides {
|
|
clientId?: string;
|
|
clientSecret?: string;
|
|
clientMetadataUrl?: string;
|
|
}
|
|
|
|
/** HTTP transports (SSE, streamable-http) can use OAuth. */
|
|
export function isOAuthCapableServerConfig(
|
|
config: { type?: string } | null | undefined,
|
|
): boolean {
|
|
if (!config) return false;
|
|
return config.type === "sse" || config.type === "streamable-http";
|
|
}
|
|
|
|
/**
|
|
* Derive OAuth / EMA / CIMD InspectorClient options from install client.json,
|
|
* per-server settings, and CLI flag overrides (flags win over client.json).
|
|
*/
|
|
export function buildRunnerClientAuthOptions(
|
|
clientConfig: ClientConfig,
|
|
savedSettings?: InspectorServerSettings,
|
|
cliOverrides?: RunnerClientConfigOverrides,
|
|
): Pick<
|
|
InspectorClientOptions,
|
|
| "oauth"
|
|
| "enterpriseManagedAuth"
|
|
| "installEnterpriseManagedAuth"
|
|
| "directAuthRecovery"
|
|
> {
|
|
const activeIdp = getActiveEnterpriseManagedAuthIdp(clientConfig);
|
|
const activeCimdUrl = getActiveCimdClientMetadataUrl(clientConfig);
|
|
|
|
const serverAuthorizationParams = savedSettings
|
|
? oauthAuthorizationParamsFromSettings(savedSettings)
|
|
: undefined;
|
|
|
|
const oauthFromServer =
|
|
savedSettings &&
|
|
(savedSettings.oauthClientId ||
|
|
savedSettings.oauthClientSecret ||
|
|
savedSettings.oauthScopes ||
|
|
serverAuthorizationParams ||
|
|
savedSettings.enterpriseManaged)
|
|
? {
|
|
...(savedSettings.oauthClientId && {
|
|
clientId: savedSettings.oauthClientId,
|
|
}),
|
|
...(savedSettings.oauthClientSecret && {
|
|
clientSecret: savedSettings.oauthClientSecret,
|
|
}),
|
|
...(savedSettings.oauthScopes && {
|
|
scope: savedSettings.oauthScopes,
|
|
}),
|
|
...(serverAuthorizationParams && {
|
|
authorizationParams: serverAuthorizationParams,
|
|
}),
|
|
...(savedSettings.enterpriseManaged && {
|
|
enterpriseManaged: true,
|
|
}),
|
|
}
|
|
: undefined;
|
|
|
|
const clientMetadataUrl =
|
|
cliOverrides?.clientMetadataUrl?.trim() || activeCimdUrl;
|
|
|
|
const oauthFromCli =
|
|
cliOverrides?.clientId || cliOverrides?.clientSecret || clientMetadataUrl
|
|
? {
|
|
...(cliOverrides?.clientId && { clientId: cliOverrides.clientId }),
|
|
...(cliOverrides?.clientSecret && {
|
|
clientSecret: cliOverrides.clientSecret,
|
|
}),
|
|
...(clientMetadataUrl && { clientMetadataUrl }),
|
|
}
|
|
: undefined;
|
|
|
|
const oauth =
|
|
oauthFromServer || oauthFromCli
|
|
? {
|
|
...(oauthFromServer ?? {}),
|
|
...(oauthFromCli ?? {}),
|
|
}
|
|
: undefined;
|
|
|
|
return {
|
|
...(oauth && { oauth }),
|
|
...(activeIdp && {
|
|
enterpriseManagedAuth: { idp: activeIdp },
|
|
}),
|
|
...(clientConfig.enterpriseManagedAuth && {
|
|
installEnterpriseManagedAuth: clientConfig.enterpriseManagedAuth,
|
|
}),
|
|
...(oauth && { directAuthRecovery: true }),
|
|
};
|
|
}
|