Merge branch 'v2/main' into v2/feat/1005-schema-lint
This commit is contained in:
commit
e005055a46
8 changed files with 1418 additions and 9 deletions
|
|
@ -12,6 +12,7 @@ import type {
|
|||
import type { OAuthStorage, SaveClientInformationOptions } from "./storage.js";
|
||||
import { generateOAuthState } from "./utils.js";
|
||||
import { applyAuthorizationParams } from "./authorizationParams.js";
|
||||
import { scopeForDeclinedRefreshGrant } from "./scopes.js";
|
||||
|
||||
/**
|
||||
* Redirect URL provider. Returns the redirect URL for OAuth flows.
|
||||
|
|
@ -105,6 +106,36 @@ export type OAuthProviderConfig = {
|
|||
* unaffected.
|
||||
*/
|
||||
authorizationParams?: Record<string, string>;
|
||||
/**
|
||||
* Whether to declare the `refresh_token` grant in the registered client
|
||||
* metadata (#2068). Defaults to `true`; set `false` to register (and
|
||||
* authorize) as an authorization-code-only client.
|
||||
*
|
||||
* This is not merely cosmetic. The SDK's `determineScope()` appends
|
||||
* `offline_access` to the effective scope whenever the authorization server
|
||||
* advertises it **and** the client metadata declares `refresh_token`, and
|
||||
* `startAuthorization()` then appends `prompt=consent` whenever
|
||||
* `offline_access` is in scope. Against Microsoft Entra ID, that forced
|
||||
* consent prompt routes a non-admin user into the admin-consent workflow and
|
||||
* fails with `AADSTS90094` even after a tenant admin has consented.
|
||||
*
|
||||
* It removes that *automatic* augmentation only. `startAuthorization()` reads
|
||||
* the scope, never `grant_types`, so an `offline_access` the caller passed in
|
||||
* `scope` — or one the resource advertises when `scope` is unset, which
|
||||
* `determineScope` falls back to — still produces the prompt. This is not on
|
||||
* its own a guarantee that `prompt=consent` is absent.
|
||||
*/
|
||||
requestRefreshToken?: boolean;
|
||||
/**
|
||||
* The scope configured for this server (mcp.json / Server Settings), as
|
||||
* distinct from the scope carried in OAuth storage after a previous grant.
|
||||
*
|
||||
* Used only to decide whether an `offline_access` in the effective scope was
|
||||
* *asked for* or merely inherited — see {@link scopeForDeclinedRefreshGrant}.
|
||||
* An explicitly configured one is honored; an inherited one is dropped from
|
||||
* the request when {@link requestRefreshToken} is false. (#2068)
|
||||
*/
|
||||
configuredScope?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
@ -126,6 +157,10 @@ export class BaseOAuthClientProvider implements OAuthClientProvider {
|
|||
public clientMetadataUrl?: string;
|
||||
/** Custom authorization-request parameters (#2018). Authorize URL only. */
|
||||
protected authorizationParams?: Record<string, string>;
|
||||
/** Declare the `refresh_token` grant in {@link clientMetadata} (#2068). */
|
||||
protected requestRefreshToken: boolean;
|
||||
/** Server-configured scope, for the #2068 `offline_access` distinction. */
|
||||
protected configuredScope?: string;
|
||||
|
||||
constructor(serverUrl: string, oauthConfig: OAuthProviderConfig) {
|
||||
this.serverUrl = serverUrl;
|
||||
|
|
@ -134,6 +169,8 @@ export class BaseOAuthClientProvider implements OAuthClientProvider {
|
|||
this.navigation = oauthConfig.navigation;
|
||||
this.clientMetadataUrl = oauthConfig.clientMetadataUrl;
|
||||
this.authorizationParams = oauthConfig.authorizationParams;
|
||||
this.requestRefreshToken = oauthConfig.requestRefreshToken ?? true;
|
||||
this.configuredScope = oauthConfig.configuredScope;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -171,6 +208,21 @@ export class BaseOAuthClientProvider implements OAuthClientProvider {
|
|||
}
|
||||
|
||||
get scope(): string | undefined {
|
||||
// #2068: filter at the point of *request*, not in storage. Covers the two
|
||||
// readers that go through this getter — `clientMetadata.scope` below, and
|
||||
// the `scope:` argument `OAuthManager.authenticate` hands to the SDK.
|
||||
//
|
||||
// It is NOT the only request path: mid-session `insufficient_scope` step-up
|
||||
// builds its union from raw storage and passes it to `mcpAuth` directly,
|
||||
// never reading this getter. `OAuthManager.handleAuthChallenge` applies the
|
||||
// same filter there. Both call `scopeForDeclinedRefreshGrant`, so the rule
|
||||
// lives in one place even though it has to be applied twice.
|
||||
if (!this.requestRefreshToken) {
|
||||
return scopeForDeclinedRefreshGrant(
|
||||
this.cachedScope,
|
||||
this.configuredScope,
|
||||
);
|
||||
}
|
||||
return this.cachedScope;
|
||||
}
|
||||
|
||||
|
|
@ -186,7 +238,14 @@ export class BaseOAuthClientProvider implements OAuthClientProvider {
|
|||
const metadata: OAuthClientMetadata = {
|
||||
redirect_uris: this.redirect_uris,
|
||||
token_endpoint_auth_method: "none",
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
// #2068: `refresh_token` is declared by default, and dropped when the
|
||||
// server's "Request refresh token" setting is off — which stops the SDK
|
||||
// *adding* `offline_access` (and so the `prompt=consent` it forces). It
|
||||
// does not remove an `offline_access` that reaches the scope another way.
|
||||
// See `OAuthProviderConfig.requestRefreshToken`.
|
||||
grant_types: this.requestRefreshToken
|
||||
? ["authorization_code", "refresh_token"]
|
||||
: ["authorization_code"],
|
||||
response_types: ["code"],
|
||||
client_name: "MCP Inspector",
|
||||
client_uri: "https://github.com/modelcontextprotocol/inspector",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,96 @@ export const computeScopeUnion = sdkComputeScopeUnion;
|
|||
*/
|
||||
export const isStrictScopeSuperset = sdkIsStrictScopeSuperset;
|
||||
|
||||
/** The scope token whose presence makes the SDK force `prompt=consent`. */
|
||||
export const OFFLINE_ACCESS_SCOPE = "offline_access";
|
||||
|
||||
/**
|
||||
* The scope to *request* when the `refresh_token` grant has been declined
|
||||
* (#2068).
|
||||
*
|
||||
* Declining the grant stops the SDK adding `offline_access`, but it cannot
|
||||
* remove one that is already in the scope — and the Inspector's own storage is
|
||||
* the way it gets there. A successful authorization persists the scope the AS
|
||||
* granted (`resolvePersistedScopeAfterGrant`), which for a default-on client
|
||||
* includes `offline_access`; the provider reloads that on the next connect and
|
||||
* hands it back to the SDK, which sees the token in scope and appends
|
||||
* `prompt=consent` again. So a server that authorized once before the opt-out
|
||||
* would keep hitting AADSTS90094 with the box unchecked — the exact failure the
|
||||
* setting exists to prevent.
|
||||
*
|
||||
* Dropping it from the *request* (not from storage) is what breaks that loop:
|
||||
* the next grant comes back without `offline_access` and the persisted scope
|
||||
* self-heals, while re-checking the box restores the old behavior from the
|
||||
* user's own configuration rather than from a value we destroyed.
|
||||
*
|
||||
* An `offline_access` the user explicitly configured is honored — they asked
|
||||
* for it, and the web form warns that it keeps the consent prompt alive. That
|
||||
* means *ensuring* it is requested, not merely declining to strip it: the
|
||||
* persisted scope can predate the configuration change (`createOAuthProvider`
|
||||
* preserves a stored scope rather than reseeding from current settings), and
|
||||
* silently omitting a scope the user typed would make both this promise and the
|
||||
* form's warning false.
|
||||
*
|
||||
* Only `offline_access` is added, never the rest of the configured scope.
|
||||
* Widening the request with unrelated configured tokens is outside what this
|
||||
* setting governs and could push an otherwise-satisfied connection into a
|
||||
* step-up authorization.
|
||||
*
|
||||
* `requiredScopes` carries scopes a `403 insufficient_scope` challenge demands
|
||||
* (SEP-2350). They are honored exactly like a configured one: the server is
|
||||
* asking for them, and stripping a scope the challenge requires would loop —
|
||||
* re-authorize, get the same challenge back, strip it again. The step-up path
|
||||
* needs this because it does not read the scope through the provider at all:
|
||||
* it unions the *raw* stored scope with the challenge and hands that to the
|
||||
* SDK directly, so an inherited `offline_access` would otherwise reappear
|
||||
* there with the setting off.
|
||||
*/
|
||||
export function scopeForDeclinedRefreshGrant(
|
||||
effectiveScope: string | undefined,
|
||||
configuredScope: string | undefined,
|
||||
requiredScopes?: readonly string[],
|
||||
): string | undefined {
|
||||
if (!effectiveScope) return effectiveScope;
|
||||
const configured = configuredScope?.trim().split(/\s+/) ?? [];
|
||||
const required = requiredScopes ?? [];
|
||||
if (
|
||||
configured.includes(OFFLINE_ACCESS_SCOPE) ||
|
||||
required.includes(OFFLINE_ACCESS_SCOPE)
|
||||
) {
|
||||
const tokens = effectiveScope
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((token) => token !== "");
|
||||
if (!tokens.includes(OFFLINE_ACCESS_SCOPE)) {
|
||||
tokens.push(OFFLINE_ACCESS_SCOPE);
|
||||
}
|
||||
return tokens.join(" ");
|
||||
}
|
||||
|
||||
const kept = effectiveScope
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter((token) => token !== "" && token !== OFFLINE_ACCESS_SCOPE);
|
||||
if (kept.length > 0) return kept.join(" ");
|
||||
|
||||
// Filtering removed everything (the persisted scope was `offline_access`
|
||||
// alone). Returning `undefined` here would be read as "nothing stored" by
|
||||
// `OAuthManager.createOAuthProvider`, whose seeding branch then *writes* the
|
||||
// configured scope to storage — turning this request-only filter into a
|
||||
// silent overwrite of the persisted value, so re-enabling the grant could no
|
||||
// longer restore it. Hand back the configured scope instead: it is the right
|
||||
// thing to request, and it leaves storage alone.
|
||||
// Cannot itself contain `offline_access`: that case returned above.
|
||||
const fallback = configuredScope?.trim();
|
||||
if (fallback) return fallback;
|
||||
|
||||
// Nothing configured either, so there is no value to preserve and the seeding
|
||||
// branch is inert (it requires a configured scope). `undefined` rather than
|
||||
// `""` so the SDK falls back to its own resolution instead of requesting an
|
||||
// empty scope.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to persist after a successful token grant (RFC 6749 §5.1).
|
||||
* When the AS returns `scope`, it is the authoritative full grant.
|
||||
|
|
|
|||
|
|
@ -87,7 +87,8 @@ export function buildRunnerClientAuthOptions(
|
|||
savedSettings.oauthScopes ||
|
||||
serverAuthorizationParams ||
|
||||
serverEndpointOverrides ||
|
||||
savedSettings.enterpriseManaged)
|
||||
savedSettings.enterpriseManaged ||
|
||||
savedSettings.oauthRequestRefreshToken === false)
|
||||
? {
|
||||
...(savedSettings.oauthClientId && {
|
||||
clientId: savedSettings.oauthClientId,
|
||||
|
|
@ -105,6 +106,11 @@ export function buildRunnerClientAuthOptions(
|
|||
...(savedSettings.enterpriseManaged && {
|
||||
enterpriseManaged: true,
|
||||
}),
|
||||
// #2068: only the explicit opt-out is forwarded; omitting the key
|
||||
// leaves the provider's default (declare `refresh_token`) in place.
|
||||
...(savedSettings.oauthRequestRefreshToken === false && {
|
||||
requestRefreshToken: false,
|
||||
}),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
|
|
|
|||
|
|
@ -6340,6 +6340,8 @@ export class InspectorClient extends InspectorClientEventTarget {
|
|||
/** Endpoint overrides applied to the discovered AS metadata (#1906). */
|
||||
authorizationUrl?: string;
|
||||
tokenUrl?: string;
|
||||
/** Declare the `refresh_token` grant in client metadata (#2068). */
|
||||
requestRefreshToken?: boolean;
|
||||
}): void {
|
||||
if (!this.oauthManager) {
|
||||
throw new Error(
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import {
|
|||
isStrictScopeSuperset,
|
||||
resolveEffectiveGrantedScope,
|
||||
resolvePersistedScopeAfterGrant,
|
||||
scopeForDeclinedRefreshGrant,
|
||||
} from "../auth/scopes.js";
|
||||
import { stepUpInsufficientScopeMessage } from "../auth/oauthUx.js";
|
||||
import type {
|
||||
|
|
@ -105,6 +106,8 @@ export class OAuthManager {
|
|||
authorizationParams?: Record<string, string>;
|
||||
authorizationUrl?: string;
|
||||
tokenUrl?: string;
|
||||
/** Declare the `refresh_token` grant in client metadata (#2068). */
|
||||
requestRefreshToken?: boolean;
|
||||
}): void {
|
||||
this.oauthConfig = {
|
||||
...this.oauthConfig,
|
||||
|
|
@ -176,6 +179,13 @@ export class OAuthManager {
|
|||
// read from mcp.json, and nothing in the flow needs to persist or union
|
||||
// them.
|
||||
authorizationParams: this.oauthConfig.authorizationParams,
|
||||
// #2068: per-server opt-out of the `refresh_token` grant. Like
|
||||
// `authorizationParams` this is pure config off mcp.json, so it lives on
|
||||
// the provider rather than in OAuth storage.
|
||||
requestRefreshToken: this.oauthConfig.requestRefreshToken,
|
||||
// #2068: lets the provider tell an `offline_access` the user asked for
|
||||
// from one inherited from a previous grant's persisted scope.
|
||||
configuredScope: this.oauthConfig.scope,
|
||||
});
|
||||
|
||||
provider.setEventTarget(this.params.getEventTarget());
|
||||
|
|
@ -305,9 +315,12 @@ export class OAuthManager {
|
|||
resourceMetadataUrl,
|
||||
});
|
||||
|
||||
// Read once: the getter filters (#2068), so this is what was actually
|
||||
// requested and what the callback leg has to persist.
|
||||
const requestedScope = provider.scope;
|
||||
const result = await mcpAuth(provider, {
|
||||
serverUrl,
|
||||
scope: provider.scope,
|
||||
scope: requestedScope,
|
||||
fetchFn: this.params.effectiveAuthFetch,
|
||||
resourceMetadataUrl,
|
||||
});
|
||||
|
|
@ -318,6 +331,21 @@ export class OAuthManager {
|
|||
);
|
||||
}
|
||||
|
||||
// #2068 — record the filtered request so `completeOAuthFlow` can persist
|
||||
// it. `resolvePersistedScopeAfterGrant` falls back to the requested scope
|
||||
// when the token response omits `scope` (RFC 6749 §5.1: omitted means the
|
||||
// grant matched the request), and without this that fallback is `undefined`
|
||||
// on the ordinary leg — only step-up ever set it. The old
|
||||
// `offline_access` would then stay in storage, so the scope never
|
||||
// self-heals and later satisfaction checks still read it as granted.
|
||||
//
|
||||
// Only when the filter is in effect. With the grant declared there is
|
||||
// nothing to heal, and persisting a request the AS did not confirm would be
|
||||
// a behavior change beyond this setting.
|
||||
if (this.oauthConfig.requestRefreshToken === false && requestedScope) {
|
||||
this.pendingAuthorizationScope = requestedScope;
|
||||
}
|
||||
|
||||
const capturedUrl = provider.getCapturedAuthUrl();
|
||||
if (!capturedUrl) {
|
||||
throw new Error("Failed to capture authorization URL");
|
||||
|
|
@ -774,13 +802,37 @@ export class OAuthManager {
|
|||
resourceMetadataUrl,
|
||||
});
|
||||
|
||||
const scopeForAuth =
|
||||
// #2068 — the step-up union is built from *raw* storage
|
||||
// (`enrichChallengeWithScopes`) and handed to the SDK directly, so it never
|
||||
// passes through the provider's filtering `scope` getter. Without this an
|
||||
// `offline_access` inherited from an earlier grant reappears here and the
|
||||
// SDK re-adds `prompt=consent` even with the setting off — the same failure
|
||||
// the option exists to prevent, on the one path that bypasses it. A scope
|
||||
// the challenge itself requires is preserved, since stripping that would
|
||||
// just re-earn the same challenge.
|
||||
const requestedScopeForAuth =
|
||||
enriched.reason === "insufficient_scope"
|
||||
? enriched.authorizationScopes?.join(" ")
|
||||
: this.oauthConfig.scope?.trim() ||
|
||||
(enriched.requiredScopes?.length
|
||||
? enriched.requiredScopes.join(" ")
|
||||
: undefined);
|
||||
const scopeForAuth =
|
||||
this.oauthConfig.requestRefreshToken === false
|
||||
? scopeForDeclinedRefreshGrant(
|
||||
requestedScopeForAuth,
|
||||
this.oauthConfig.scope,
|
||||
// The RAW challenge scopes, not `enriched.requiredScopes`:
|
||||
// `enrichChallengeWithScopes` narrows that to the *missing* subset,
|
||||
// so a challenge requiring `offline_access tools:write` against a
|
||||
// stored scope that already has `offline_access` arrives here as
|
||||
// just `["tools:write"]`. Passing the narrowed list would read the
|
||||
// server's explicit requirement as inherited and strip it — then
|
||||
// re-authorization earns the same challenge forever, which is the
|
||||
// loop this argument exists to prevent.
|
||||
challenge.requiredScopes,
|
||||
)
|
||||
: requestedScopeForAuth;
|
||||
|
||||
provider.setSuppressAuthorizationNavigation(true);
|
||||
let result: Awaited<ReturnType<typeof mcpAuth>>;
|
||||
|
|
|
|||
|
|
@ -1391,6 +1391,7 @@ export function createRemoteApp(
|
|||
authorizationUrl?: string;
|
||||
tokenUrl?: string;
|
||||
enterpriseManaged?: boolean;
|
||||
requestRefreshToken?: boolean;
|
||||
} => {
|
||||
if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
|
||||
const o = v as Record<string, unknown>;
|
||||
|
|
@ -1418,6 +1419,14 @@ export function createRemoteApp(
|
|||
) {
|
||||
return false;
|
||||
}
|
||||
// #2068 — shape only; the read side keeps just an explicit `false`, so a
|
||||
// stray `true` from a hand-edited file reads back as the default anyway.
|
||||
if (
|
||||
o.requestRefreshToken !== undefined &&
|
||||
typeof o.requestRefreshToken !== "boolean"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
// A roots array: each entry must have a string `uri` and, when present, a
|
||||
|
|
@ -1531,7 +1540,7 @@ export function createRemoteApp(
|
|||
if ("oauth" in valObj && !isOauthObject(valObj.oauth)) {
|
||||
logWarn(
|
||||
{ route: "/api/servers", id, droppedKey: "oauth" },
|
||||
"Dropping malformed `oauth` field — expected `{ clientId?, clientSecret?, scopes?, authorizationParams?, authorizationUrl?, tokenUrl?, enterpriseManaged?, onInsufficientScope? }`.",
|
||||
"Dropping malformed `oauth` field — expected `{ clientId?, clientSecret?, scopes?, authorizationParams?, authorizationUrl?, tokenUrl?, enterpriseManaged?, onInsufficientScope?, requestRefreshToken? }`.",
|
||||
);
|
||||
delete valObj.oauth;
|
||||
}
|
||||
|
|
@ -1832,6 +1841,15 @@ export function createRemoteApp(
|
|||
error: "settings.enterpriseManaged must be a boolean",
|
||||
};
|
||||
}
|
||||
if (
|
||||
obj.oauthRequestRefreshToken !== undefined &&
|
||||
typeof obj.oauthRequestRefreshToken !== "boolean"
|
||||
) {
|
||||
return {
|
||||
ok: false,
|
||||
error: "settings.oauthRequestRefreshToken must be a boolean",
|
||||
};
|
||||
}
|
||||
if (
|
||||
obj.oauthOnInsufficientScope !== undefined &&
|
||||
obj.oauthOnInsufficientScope !== "reauthorize" &&
|
||||
|
|
@ -1959,6 +1977,11 @@ export function createRemoteApp(
|
|||
if (obj.enterpriseManaged === true) {
|
||||
value.enterpriseManaged = true;
|
||||
}
|
||||
// #2068: the default is on, so only the explicit opt-out is carried; a
|
||||
// `true` on the wire reads back as unset, which means the same thing.
|
||||
if (obj.oauthRequestRefreshToken === false) {
|
||||
value.oauthRequestRefreshToken = false;
|
||||
}
|
||||
if (
|
||||
obj.oauthOnInsufficientScope === "reauthorize" ||
|
||||
obj.oauthOnInsufficientScope === "throw"
|
||||
|
|
|
|||
|
|
@ -555,6 +555,12 @@ export function storedFieldsToInspectorSettings(
|
|||
if (stored.oauth?.enterpriseManaged === true) {
|
||||
settings.enterpriseManaged = true;
|
||||
}
|
||||
// Inverted from the flags above: the default is *on*, so only an explicit
|
||||
// `false` is carried through. Anything else on disk (absent, or a
|
||||
// hand-edited non-boolean) reads back as unset, i.e. the default. (#2068)
|
||||
if (stored.oauth?.requestRefreshToken === false) {
|
||||
settings.oauthRequestRefreshToken = false;
|
||||
}
|
||||
// Mirror the stdio working directory for the form. Like the OAuth fields, an
|
||||
// empty string coerces to absent so the form's "(inherit)" placeholder shows.
|
||||
if (stored.cwd) settings.cwd = stored.cwd;
|
||||
|
|
@ -682,6 +688,12 @@ export function inspectorSettingsToStoredFields(
|
|||
if (settings.enterpriseManaged === true) {
|
||||
oauthFields.enterpriseManaged = true;
|
||||
}
|
||||
// Only the non-default (off) is written; an absent field reads back as the
|
||||
// default (on), so writing `true` would inject the key into files that never
|
||||
// set it and break byte-stable round-trips. (#2068)
|
||||
if (settings.oauthRequestRefreshToken === false) {
|
||||
oauthFields.requestRefreshToken = false;
|
||||
}
|
||||
if (Object.keys(oauthFields).length > 0) {
|
||||
out.oauth = oauthFields;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -181,6 +181,18 @@ export type StoredMCPServer = MCPServerConfig & {
|
|||
enterpriseManaged?: boolean;
|
||||
/** SEP-2350 step-up policy for `403 insufficient_scope` (default `reauthorize`). */
|
||||
onInsufficientScope?: OnInsufficientScopePolicy;
|
||||
/**
|
||||
* Whether the Inspector declares the `refresh_token` grant when it
|
||||
* registers. Defaults to `true`; only `false` is written to disk, so an
|
||||
* entry that never touched the setting keeps a minimal diff. Turning it off
|
||||
* stops the SDK's *automatic* `offline_access` augmentation, and so the
|
||||
* `prompt=consent` that scope forces — but only that one: an
|
||||
* `offline_access` from `scopes` here, or from the resource's advertised
|
||||
* scopes when `scopes` is unset, still reaches the authorization request.
|
||||
* See {@link InspectorServerSettings.oauthRequestRefreshToken}.
|
||||
* Inspector-specific. (#2068)
|
||||
*/
|
||||
requestRefreshToken?: boolean;
|
||||
/**
|
||||
* Custom query parameters appended to the OAuth **authorization request**
|
||||
* (never the token request) — e.g. Keycloak's `kc_idp_hint`, OIDC's
|
||||
|
|
@ -589,6 +601,11 @@ export interface OAuthSettings {
|
|||
tokenUrl?: string;
|
||||
enterpriseManaged?: boolean;
|
||||
onInsufficientScope?: OnInsufficientScopePolicy;
|
||||
/**
|
||||
* Whether to declare the `refresh_token` grant (#2068). Optional for the same
|
||||
* reason as `authorizationParams`; `undefined` means the default, on.
|
||||
*/
|
||||
requestRefreshToken?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -774,6 +791,26 @@ export interface InspectorServerSettings {
|
|||
* server's HTTP transport. Defaults to `reauthorize` when unset.
|
||||
*/
|
||||
oauthOnInsufficientScope?: OnInsufficientScopePolicy;
|
||||
/**
|
||||
* Whether the Inspector declares the `refresh_token` grant in its OAuth
|
||||
* client metadata for this server. `undefined` (the default) means on;
|
||||
* persisted as `oauth.requestRefreshToken` only when explicitly off. Turning
|
||||
* it off drops the SDK's *automatic* `offline_access` scope and so the
|
||||
* `prompt=consent` it forces — the Entra admin-consent failure in #2068.
|
||||
*
|
||||
* Only the automatic one: `startAuthorization` adds `prompt=consent` for any
|
||||
* `offline_access` in the effective scope without consulting `grant_types`,
|
||||
* so a scope the user configured (or one the resource advertises when the
|
||||
* scope field is blank) still forces the prompt.
|
||||
*
|
||||
* Read at connect time like the rest of the OAuth block, so a live client
|
||||
* keeps the metadata it was built with. It changes what the Inspector
|
||||
* declares, not state the authorization server already holds: an existing
|
||||
* registration still lists the grant, and a refresh token issued earlier
|
||||
* stays usable (the SDK's refresh path never consults `grant_types`) until
|
||||
* the stored OAuth state is cleared.
|
||||
*/
|
||||
oauthRequestRefreshToken?: boolean;
|
||||
/**
|
||||
* When true, connect via the configured enterprise IdP (EMA) instead of
|
||||
* interactive OAuth to the MCP authorization server. Per-server OAuth
|
||||
|
|
@ -1170,6 +1207,11 @@ export interface InspectorClientOptions {
|
|||
*/
|
||||
authorizationUrl?: string;
|
||||
tokenUrl?: string;
|
||||
/**
|
||||
* Declare the `refresh_token` grant in the registered client metadata
|
||||
* (#2068). Defaults to `true` when omitted.
|
||||
*/
|
||||
requestRefreshToken?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in a new issue