-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix(gateway): add demo GIF assets for gateway lab samples #1569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,8 +14,8 @@ dist/ | |
| downloads/ | ||
| eggs/ | ||
| .eggs/ | ||
| lib/ | ||
| lib64/ | ||
| /lib/ | ||
| /lib64/ | ||
| parts/ | ||
| sdist/ | ||
| var/ | ||
|
|
||
| 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 failureCode scanning / CodeQL Clear text storage of sensitive information High
This stores sensitive data returned by
a call to getItem Error loading related location Loading This stores sensitive data returned by a call to getItem Error loading related location Loading This stores sensitive data returned by an access to oauthClientId Error loading related location Loading This stores sensitive data returned by an access to oauthClientSecret Error loading related location Loading This stores sensitive data returned by an access to oauthClientId Error loading related location Loading This stores sensitive data returned by an access to oauthClientSecret Error loading related location Loading This stores sensitive data returned by an access to oauthClientId Error loading related location Loading This stores sensitive data returned by an access to oauthClientSecret Error loading related location Loading This stores sensitive data returned by an access to oauthClientId Error loading related location Loading This stores sensitive data returned by an access to oauthClientSecret Error loading related location Loading |
||
| }; | ||
|
|
||
| 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 failureCode scanning / CodeQL Clear text storage of sensitive information High
This stores sensitive data returned by
a call to getItem Error loading related location Loading This stores sensitive data returned by an access to oauthScope Error loading related location Loading This stores sensitive data returned by an access to oauthScope Error loading related location Loading This stores sensitive data returned by an access to oauthScopes Error loading related location Loading This stores sensitive data returned by an access to oauthScope Error loading related location Loading This stores sensitive data returned by an access to oauthScope Error loading related location Loading This stores sensitive data returned by an access to oauthScope Error loading related location Loading This stores sensitive data returned by an access to oauthScope Error loading related location Loading |
||
|
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 failureCode scanning / CodeQL Clear text storage of sensitive information High
This stores sensitive data returned by
an access to oauthMachine Error loading related location Loading |
||
|
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), | ||
| ); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.