Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ lib/
!05-blueprints/**/lib/
!02-use-cases/visa-b2b-account-payable-agent/infrastructure/lib/
!04-infrastructure-as-code/cdk/typescript/knowledge-base-rag-agent/infrastructure/lib/
!01-features/07-centralize-and-govern-your-ai-infrastructure/01-gateway/05-community/gateway-mcp-inspector/client/src/lib/
!06-workshops/01-AgentCore-runtime/01-hosting-agent/05-java-agents/01-springai-with-bedrock-model/infra/lib/
!06-workshops/01-AgentCore-runtime/01-hosting-agent/05-java-agents/02-embabel-with-bedrock-model/infra/lib/
lib64/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ dist/
downloads/
eggs/
.eggs/
lib/
lib64/
/lib/
/lib64/
parts/
sdist/
var/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ const GatewaySelector = ({
);
const data = await response.json();
if (data.gatewayUrl) {
onSelect(gw.gatewayId, data.gatewayUrl);
const url = data.gatewayUrl.endsWith("/mcp")
? data.gatewayUrl
: `${data.gatewayUrl.replace(/\/$/, "")}/mcp`;
onSelect(gw.gatewayId, url);
} else {
setError(data.error || `Could not resolve URL for ${gw.name}`);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
OAuthMetadata,
OAuthClientInformationFull,
OAuthClientInformation,
OAuthTokens,
OAuthProtectedResourceMetadata,
} from "@modelcontextprotocol/sdk/shared/auth.js";

// OAuth flow steps
export type OAuthStep =
| "metadata_discovery"
| "client_registration"
| "authorization_redirect"
| "authorization_code"
| "token_request"
| "complete";

// Message types for inline feedback
export type MessageType = "success" | "error" | "info";

export interface StatusMessage {
type: MessageType;
message: string;
}

// Single state interface for OAuth state
export interface AuthDebuggerState {
isInitiatingAuth: boolean;
oauthTokens: OAuthTokens | null;
oauthStep: OAuthStep;
resourceMetadata: OAuthProtectedResourceMetadata | null;
resourceMetadataError: Error | null;
resource: URL | null;
authServerUrl: URL | null;
oauthMetadata: OAuthMetadata | null;
oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null;
authorizationUrl: URL | null;
authorizationCode: string;
latestError: Error | null;
statusMessage: StatusMessage | null;
validationError: string | null;
}

export const EMPTY_DEBUGGER_STATE: AuthDebuggerState = {
isInitiatingAuth: false,
oauthTokens: null,
oauthStep: "metadata_discovery",
oauthMetadata: null,
resourceMetadata: null,
resourceMetadataError: null,
resource: null,
authServerUrl: null,
oauthClientInfo: null,
authorizationUrl: null,
authorizationCode: "",
latestError: null,
statusMessage: null,
validationError: null,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
import { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
import {
OAuthClientInformationSchema,
OAuthClientInformation,
OAuthTokens,
OAuthTokensSchema,
OAuthClientMetadata,
OAuthMetadata,
OAuthProtectedResourceMetadata,
} from "@modelcontextprotocol/sdk/shared/auth.js";
import { discoverAuthorizationServerMetadata } from "@modelcontextprotocol/sdk/client/auth.js";
import { SESSION_KEYS, getServerSpecificKey } from "./constants";
import { generateOAuthState } from "@/utils/oauthUtils";
import { validateRedirectUrl } from "@/utils/urlValidation";

/**
* Discovers OAuth scopes from server metadata, with preference for resource metadata scopes
* @param serverUrl - The MCP server URL
* @param resourceMetadata - Optional resource metadata containing preferred scopes
* @returns Promise resolving to space-separated scope string or undefined
*/
export const discoverScopes = async (
serverUrl: string,
resourceMetadata?: OAuthProtectedResourceMetadata,
): Promise<string | undefined> => {
try {
const metadata = await discoverAuthorizationServerMetadata(
new URL("/", serverUrl),
);

// Prefer resource metadata scopes, but fall back to OAuth metadata if empty
const resourceScopes = resourceMetadata?.scopes_supported;
const oauthScopes = metadata?.scopes_supported;

const scopesSupported =
resourceScopes && resourceScopes.length > 0
? resourceScopes
: oauthScopes;

return scopesSupported && scopesSupported.length > 0
? scopesSupported.join(" ")
: undefined;
} catch (error) {
console.debug("OAuth scope discovery failed:", error);
return undefined;
}
};

export const getClientInformationFromSessionStorage = async ({
serverUrl,
isPreregistered,
}: {
serverUrl: string;
isPreregistered?: boolean;
}) => {
const key = getServerSpecificKey(
isPreregistered
? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION
: SESSION_KEYS.CLIENT_INFORMATION,
serverUrl,
);

const value = sessionStorage.getItem(key);
if (!value) {
return undefined;
}

return await OAuthClientInformationSchema.parseAsync(JSON.parse(value));
};

export const saveClientInformationToSessionStorage = ({
serverUrl,
clientInformation,
isPreregistered,
}: {
serverUrl: string;
clientInformation: OAuthClientInformation;
isPreregistered?: boolean;
}) => {
const key = getServerSpecificKey(
isPreregistered
? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION
: SESSION_KEYS.CLIENT_INFORMATION,
serverUrl,
);
sessionStorage.setItem(key, JSON.stringify(clientInformation));

Check failure

Code scanning / CodeQL

Clear text storage of sensitive information High

This stores sensitive data returned by
a call to getItem
as clear text.
This stores sensitive data returned by
a call to getItem
as clear text.
This stores sensitive data returned by
an access to oauthClientId
as clear text.
This stores sensitive data returned by
an access to oauthClientSecret
as clear text.
This stores sensitive data returned by
an access to oauthClientId
as clear text.
This stores sensitive data returned by
an access to oauthClientSecret
as clear text.
This stores sensitive data returned by
an access to oauthClientId
as clear text.
This stores sensitive data returned by
an access to oauthClientSecret
as clear text.
This stores sensitive data returned by
an access to oauthClientId
as clear text.
This stores sensitive data returned by
an access to oauthClientSecret
as clear text.
Comment thread
EashanKaushik marked this conversation as resolved.
Dismissed
};

export const clearClientInformationFromSessionStorage = ({
serverUrl,
isPreregistered,
}: {
serverUrl: string;
isPreregistered?: boolean;
}) => {
const key = getServerSpecificKey(
isPreregistered
? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION
: SESSION_KEYS.CLIENT_INFORMATION,
serverUrl,
);
sessionStorage.removeItem(key);
};

export const getScopeFromSessionStorage = (
serverUrl: string,
): string | undefined => {
const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl);
const value = sessionStorage.getItem(key);
return value || undefined;
};

export const saveScopeToSessionStorage = (
serverUrl: string,
scope: string | undefined,
) => {
const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl);
if (scope) {
sessionStorage.setItem(key, scope);

Check failure

Code scanning / CodeQL

Clear text storage of sensitive information High

This stores sensitive data returned by
a call to getItem
as clear text.
This stores sensitive data returned by
an access to oauthScope
as clear text.
This stores sensitive data returned by
an access to oauthScope
as clear text.
This stores sensitive data returned by
an access to oauthScopes
as clear text.
This stores sensitive data returned by
an access to oauthScope
as clear text.
This stores sensitive data returned by
an access to oauthScope
as clear text.
This stores sensitive data returned by
an access to oauthScope
as clear text.
This stores sensitive data returned by
an access to oauthScope
as clear text.
Comment thread
EashanKaushik marked this conversation as resolved.
Dismissed
} else {
sessionStorage.removeItem(key);
}
};

export const clearScopeFromSessionStorage = (serverUrl: string) => {
const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl);
sessionStorage.removeItem(key);
};

export class InspectorOAuthClientProvider implements OAuthClientProvider {
constructor(protected serverUrl: string) {
// Save the server URL to session storage
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl);

Check failure

Code scanning / CodeQL

Clear text storage of sensitive information High

This stores sensitive data returned by
an access to oauthMachine
as clear text.
Comment thread
EashanKaushik marked this conversation as resolved.
Dismissed
}

get scope(): string | undefined {
return getScopeFromSessionStorage(this.serverUrl);
}

get redirectUrl() {
return window.location.origin + "/oauth/callback";
}

get debugRedirectUrl() {
return window.location.origin + "/oauth/callback/debug";
}

get redirect_uris() {
// Normally register both redirect URIs to support both normal and debug flows
// In debug subclass, redirectUrl may be the same as debugRedirectUrl, so remove duplicates
// See: https://github.com/modelcontextprotocol/inspector/issues/825
return [...new Set([this.redirectUrl, this.debugRedirectUrl])];
}

get clientMetadata(): OAuthClientMetadata {
const metadata: OAuthClientMetadata = {
redirect_uris: this.redirect_uris,
token_endpoint_auth_method: "none",
grant_types: ["authorization_code", "refresh_token"],
response_types: ["code"],
client_name: "MCP Inspector",
client_uri: "https://github.com/modelcontextprotocol/inspector",
};

// Only include scope if it's defined and non-empty
// Per OAuth spec, omit the scope field entirely if no scopes are requested
if (this.scope) {
metadata.scope = this.scope;
}

return metadata;
}

state(): string | Promise<string> {
return generateOAuthState();
}

async clientInformation() {
// Try to get the preregistered client information from session storage first
const preregisteredClientInformation =
await getClientInformationFromSessionStorage({
serverUrl: this.serverUrl,
isPreregistered: true,
});

// If no preregistered client information is found, get the dynamically registered client information
return (
preregisteredClientInformation ??
(await getClientInformationFromSessionStorage({
serverUrl: this.serverUrl,
isPreregistered: false,
}))
);
}

saveClientInformation(clientInformation: OAuthClientInformation) {
// Save the dynamically registered client information to session storage
saveClientInformationToSessionStorage({
serverUrl: this.serverUrl,
clientInformation,
isPreregistered: false,
});
}

async tokens() {
const key = getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl);
const tokens = sessionStorage.getItem(key);
if (!tokens) {
return undefined;
}

return await OAuthTokensSchema.parseAsync(JSON.parse(tokens));
}

saveTokens(tokens: OAuthTokens) {
const key = getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl);
sessionStorage.setItem(key, JSON.stringify(tokens));
}

redirectToAuthorization(authorizationUrl: URL) {
// Validate the URL using the shared utility
validateRedirectUrl(authorizationUrl.href);
window.location.href = authorizationUrl.href;
}

saveCodeVerifier(codeVerifier: string) {
const key = getServerSpecificKey(
SESSION_KEYS.CODE_VERIFIER,
this.serverUrl,
);
sessionStorage.setItem(key, codeVerifier);
}

codeVerifier() {
const key = getServerSpecificKey(
SESSION_KEYS.CODE_VERIFIER,
this.serverUrl,
);
const verifier = sessionStorage.getItem(key);
if (!verifier) {
throw new Error("No code verifier saved for session");
}

return verifier;
}

clear() {
clearClientInformationFromSessionStorage({
serverUrl: this.serverUrl,
isPreregistered: false,
});
sessionStorage.removeItem(
getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl),
);
sessionStorage.removeItem(
getServerSpecificKey(SESSION_KEYS.CODE_VERIFIER, this.serverUrl),
);
}
}

// Overrides redirect URL to use the debug endpoint and allows saving server OAuth metadata to
// display in debug UI.
export class DebugInspectorOAuthClientProvider extends InspectorOAuthClientProvider {
get redirectUrl(): string {
// We can use the debug redirect URL here because it was already registered
// in the parent class's clientMetadata along with the normal redirect URL
return this.debugRedirectUrl;
}

saveServerMetadata(metadata: OAuthMetadata) {
const key = getServerSpecificKey(
SESSION_KEYS.SERVER_METADATA,
this.serverUrl,
);
sessionStorage.setItem(key, JSON.stringify(metadata));
}

getServerMetadata(): OAuthMetadata | null {
const key = getServerSpecificKey(
SESSION_KEYS.SERVER_METADATA,
this.serverUrl,
);
const metadata = sessionStorage.getItem(key);
if (!metadata) {
return null;
}
return JSON.parse(metadata);
}

clear() {
super.clear();
sessionStorage.removeItem(
getServerSpecificKey(SESSION_KEYS.SERVER_METADATA, this.serverUrl),
);
}
}
Loading
Loading