Skip to content

Commit 2ecdc24

Browse files
fix(gateway): add demo GIF assets for gateway lab samples (#1569)
* fix(gateway): add demo GIF assets for gateway lab samples LFS-tracked GIF demos were missing actual binary content. Added all 21 demo GIFs covering attach-targets, inbound-auth, and advanced-concepts labs. * fix(gateway-inspector): add missing client/src/lib sources and fix /mcp URL The lib/ gitignore rule was unintentionally excluding the inspector's TypeScript source files, causing build failures. Fixed by narrowing the ignore rules and adding the missing client library modules. Also ensures gateway URLs always include the /mcp suffix when selected.
1 parent 3a8d535 commit 2ecdc24

18 files changed

Lines changed: 2562 additions & 57 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ lib/
5454
!05-blueprints/**/lib/
5555
!02-use-cases/visa-b2b-account-payable-agent/infrastructure/lib/
5656
!04-infrastructure-as-code/cdk/typescript/knowledge-base-rag-agent/infrastructure/lib/
57+
!01-features/07-centralize-and-govern-your-ai-infrastructure/01-gateway/05-community/gateway-mcp-inspector/client/src/lib/
5758
!06-workshops/01-AgentCore-runtime/01-hosting-agent/05-java-agents/01-springai-with-bedrock-model/infra/lib/
5859
!06-workshops/01-AgentCore-runtime/01-hosting-agent/05-java-agents/02-embabel-with-bedrock-model/infra/lib/
5960
!02-use-cases/lakehouse-agent/deployment/advanced-agentcore-policy-gateway-interceptor/lib/

01-features/07-centralize-and-govern-your-ai-infrastructure/01-gateway/.gitignore

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ dist/
1414
downloads/
1515
eggs/
1616
.eggs/
17-
lib/
18-
lib64/
17+
/lib/
18+
/lib64/
1919
parts/
2020
sdist/
2121
var/

01-features/07-centralize-and-govern-your-ai-infrastructure/01-gateway/05-community/gateway-mcp-inspector/client/src/components/GatewaySelector.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ const GatewaySelector = ({
7070
);
7171
const data = await response.json();
7272
if (data.gatewayUrl) {
73-
onSelect(gw.gatewayId, data.gatewayUrl);
73+
const url = data.gatewayUrl.endsWith("/mcp")
74+
? data.gatewayUrl
75+
: `${data.gatewayUrl.replace(/\/$/, "")}/mcp`;
76+
onSelect(gw.gatewayId, url);
7477
} else {
7578
setError(data.error || `Could not resolve URL for ${gw.name}`);
7679
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {
2+
OAuthMetadata,
3+
OAuthClientInformationFull,
4+
OAuthClientInformation,
5+
OAuthTokens,
6+
OAuthProtectedResourceMetadata,
7+
} from "@modelcontextprotocol/sdk/shared/auth.js";
8+
9+
// OAuth flow steps
10+
export type OAuthStep =
11+
| "metadata_discovery"
12+
| "client_registration"
13+
| "authorization_redirect"
14+
| "authorization_code"
15+
| "token_request"
16+
| "complete";
17+
18+
// Message types for inline feedback
19+
export type MessageType = "success" | "error" | "info";
20+
21+
export interface StatusMessage {
22+
type: MessageType;
23+
message: string;
24+
}
25+
26+
// Single state interface for OAuth state
27+
export interface AuthDebuggerState {
28+
isInitiatingAuth: boolean;
29+
oauthTokens: OAuthTokens | null;
30+
oauthStep: OAuthStep;
31+
resourceMetadata: OAuthProtectedResourceMetadata | null;
32+
resourceMetadataError: Error | null;
33+
resource: URL | null;
34+
authServerUrl: URL | null;
35+
oauthMetadata: OAuthMetadata | null;
36+
oauthClientInfo: OAuthClientInformationFull | OAuthClientInformation | null;
37+
authorizationUrl: URL | null;
38+
authorizationCode: string;
39+
latestError: Error | null;
40+
statusMessage: StatusMessage | null;
41+
validationError: string | null;
42+
}
43+
44+
export const EMPTY_DEBUGGER_STATE: AuthDebuggerState = {
45+
isInitiatingAuth: false,
46+
oauthTokens: null,
47+
oauthStep: "metadata_discovery",
48+
oauthMetadata: null,
49+
resourceMetadata: null,
50+
resourceMetadataError: null,
51+
resource: null,
52+
authServerUrl: null,
53+
oauthClientInfo: null,
54+
authorizationUrl: null,
55+
authorizationCode: "",
56+
latestError: null,
57+
statusMessage: null,
58+
validationError: null,
59+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
2+
import {
3+
OAuthClientInformationSchema,
4+
OAuthClientInformation,
5+
OAuthTokens,
6+
OAuthTokensSchema,
7+
OAuthClientMetadata,
8+
OAuthMetadata,
9+
OAuthProtectedResourceMetadata,
10+
} from "@modelcontextprotocol/sdk/shared/auth.js";
11+
import { discoverAuthorizationServerMetadata } from "@modelcontextprotocol/sdk/client/auth.js";
12+
import { SESSION_KEYS, getServerSpecificKey } from "./constants";
13+
import { generateOAuthState } from "@/utils/oauthUtils";
14+
import { validateRedirectUrl } from "@/utils/urlValidation";
15+
16+
/**
17+
* Discovers OAuth scopes from server metadata, with preference for resource metadata scopes
18+
* @param serverUrl - The MCP server URL
19+
* @param resourceMetadata - Optional resource metadata containing preferred scopes
20+
* @returns Promise resolving to space-separated scope string or undefined
21+
*/
22+
export const discoverScopes = async (
23+
serverUrl: string,
24+
resourceMetadata?: OAuthProtectedResourceMetadata,
25+
): Promise<string | undefined> => {
26+
try {
27+
const metadata = await discoverAuthorizationServerMetadata(
28+
new URL("/", serverUrl),
29+
);
30+
31+
// Prefer resource metadata scopes, but fall back to OAuth metadata if empty
32+
const resourceScopes = resourceMetadata?.scopes_supported;
33+
const oauthScopes = metadata?.scopes_supported;
34+
35+
const scopesSupported =
36+
resourceScopes && resourceScopes.length > 0
37+
? resourceScopes
38+
: oauthScopes;
39+
40+
return scopesSupported && scopesSupported.length > 0
41+
? scopesSupported.join(" ")
42+
: undefined;
43+
} catch (error) {
44+
console.debug("OAuth scope discovery failed:", error);
45+
return undefined;
46+
}
47+
};
48+
49+
export const getClientInformationFromSessionStorage = async ({
50+
serverUrl,
51+
isPreregistered,
52+
}: {
53+
serverUrl: string;
54+
isPreregistered?: boolean;
55+
}) => {
56+
const key = getServerSpecificKey(
57+
isPreregistered
58+
? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION
59+
: SESSION_KEYS.CLIENT_INFORMATION,
60+
serverUrl,
61+
);
62+
63+
const value = sessionStorage.getItem(key);
64+
if (!value) {
65+
return undefined;
66+
}
67+
68+
return await OAuthClientInformationSchema.parseAsync(JSON.parse(value));
69+
};
70+
71+
export const saveClientInformationToSessionStorage = ({
72+
serverUrl,
73+
clientInformation,
74+
isPreregistered,
75+
}: {
76+
serverUrl: string;
77+
clientInformation: OAuthClientInformation;
78+
isPreregistered?: boolean;
79+
}) => {
80+
const key = getServerSpecificKey(
81+
isPreregistered
82+
? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION
83+
: SESSION_KEYS.CLIENT_INFORMATION,
84+
serverUrl,
85+
);
86+
sessionStorage.setItem(key, JSON.stringify(clientInformation));
87+
};
88+
89+
export const clearClientInformationFromSessionStorage = ({
90+
serverUrl,
91+
isPreregistered,
92+
}: {
93+
serverUrl: string;
94+
isPreregistered?: boolean;
95+
}) => {
96+
const key = getServerSpecificKey(
97+
isPreregistered
98+
? SESSION_KEYS.PREREGISTERED_CLIENT_INFORMATION
99+
: SESSION_KEYS.CLIENT_INFORMATION,
100+
serverUrl,
101+
);
102+
sessionStorage.removeItem(key);
103+
};
104+
105+
export const getScopeFromSessionStorage = (
106+
serverUrl: string,
107+
): string | undefined => {
108+
const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl);
109+
const value = sessionStorage.getItem(key);
110+
return value || undefined;
111+
};
112+
113+
export const saveScopeToSessionStorage = (
114+
serverUrl: string,
115+
scope: string | undefined,
116+
) => {
117+
const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl);
118+
if (scope) {
119+
sessionStorage.setItem(key, scope);
120+
} else {
121+
sessionStorage.removeItem(key);
122+
}
123+
};
124+
125+
export const clearScopeFromSessionStorage = (serverUrl: string) => {
126+
const key = getServerSpecificKey(SESSION_KEYS.SCOPE, serverUrl);
127+
sessionStorage.removeItem(key);
128+
};
129+
130+
export class InspectorOAuthClientProvider implements OAuthClientProvider {
131+
constructor(protected serverUrl: string) {
132+
// Save the server URL to session storage
133+
sessionStorage.setItem(SESSION_KEYS.SERVER_URL, serverUrl);
134+
}
135+
136+
get scope(): string | undefined {
137+
return getScopeFromSessionStorage(this.serverUrl);
138+
}
139+
140+
get redirectUrl() {
141+
return window.location.origin + "/oauth/callback";
142+
}
143+
144+
get debugRedirectUrl() {
145+
return window.location.origin + "/oauth/callback/debug";
146+
}
147+
148+
get redirect_uris() {
149+
// Normally register both redirect URIs to support both normal and debug flows
150+
// In debug subclass, redirectUrl may be the same as debugRedirectUrl, so remove duplicates
151+
// See: https://github.com/modelcontextprotocol/inspector/issues/825
152+
return [...new Set([this.redirectUrl, this.debugRedirectUrl])];
153+
}
154+
155+
get clientMetadata(): OAuthClientMetadata {
156+
const metadata: OAuthClientMetadata = {
157+
redirect_uris: this.redirect_uris,
158+
token_endpoint_auth_method: "none",
159+
grant_types: ["authorization_code", "refresh_token"],
160+
response_types: ["code"],
161+
client_name: "MCP Inspector",
162+
client_uri: "https://github.com/modelcontextprotocol/inspector",
163+
};
164+
165+
// Only include scope if it's defined and non-empty
166+
// Per OAuth spec, omit the scope field entirely if no scopes are requested
167+
if (this.scope) {
168+
metadata.scope = this.scope;
169+
}
170+
171+
return metadata;
172+
}
173+
174+
state(): string | Promise<string> {
175+
return generateOAuthState();
176+
}
177+
178+
async clientInformation() {
179+
// Try to get the preregistered client information from session storage first
180+
const preregisteredClientInformation =
181+
await getClientInformationFromSessionStorage({
182+
serverUrl: this.serverUrl,
183+
isPreregistered: true,
184+
});
185+
186+
// If no preregistered client information is found, get the dynamically registered client information
187+
return (
188+
preregisteredClientInformation ??
189+
(await getClientInformationFromSessionStorage({
190+
serverUrl: this.serverUrl,
191+
isPreregistered: false,
192+
}))
193+
);
194+
}
195+
196+
saveClientInformation(clientInformation: OAuthClientInformation) {
197+
// Save the dynamically registered client information to session storage
198+
saveClientInformationToSessionStorage({
199+
serverUrl: this.serverUrl,
200+
clientInformation,
201+
isPreregistered: false,
202+
});
203+
}
204+
205+
async tokens() {
206+
const key = getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl);
207+
const tokens = sessionStorage.getItem(key);
208+
if (!tokens) {
209+
return undefined;
210+
}
211+
212+
return await OAuthTokensSchema.parseAsync(JSON.parse(tokens));
213+
}
214+
215+
saveTokens(tokens: OAuthTokens) {
216+
const key = getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl);
217+
sessionStorage.setItem(key, JSON.stringify(tokens));
218+
}
219+
220+
redirectToAuthorization(authorizationUrl: URL) {
221+
// Validate the URL using the shared utility
222+
validateRedirectUrl(authorizationUrl.href);
223+
window.location.href = authorizationUrl.href;
224+
}
225+
226+
saveCodeVerifier(codeVerifier: string) {
227+
const key = getServerSpecificKey(
228+
SESSION_KEYS.CODE_VERIFIER,
229+
this.serverUrl,
230+
);
231+
sessionStorage.setItem(key, codeVerifier);
232+
}
233+
234+
codeVerifier() {
235+
const key = getServerSpecificKey(
236+
SESSION_KEYS.CODE_VERIFIER,
237+
this.serverUrl,
238+
);
239+
const verifier = sessionStorage.getItem(key);
240+
if (!verifier) {
241+
throw new Error("No code verifier saved for session");
242+
}
243+
244+
return verifier;
245+
}
246+
247+
clear() {
248+
clearClientInformationFromSessionStorage({
249+
serverUrl: this.serverUrl,
250+
isPreregistered: false,
251+
});
252+
sessionStorage.removeItem(
253+
getServerSpecificKey(SESSION_KEYS.TOKENS, this.serverUrl),
254+
);
255+
sessionStorage.removeItem(
256+
getServerSpecificKey(SESSION_KEYS.CODE_VERIFIER, this.serverUrl),
257+
);
258+
}
259+
}
260+
261+
// Overrides redirect URL to use the debug endpoint and allows saving server OAuth metadata to
262+
// display in debug UI.
263+
export class DebugInspectorOAuthClientProvider extends InspectorOAuthClientProvider {
264+
get redirectUrl(): string {
265+
// We can use the debug redirect URL here because it was already registered
266+
// in the parent class's clientMetadata along with the normal redirect URL
267+
return this.debugRedirectUrl;
268+
}
269+
270+
saveServerMetadata(metadata: OAuthMetadata) {
271+
const key = getServerSpecificKey(
272+
SESSION_KEYS.SERVER_METADATA,
273+
this.serverUrl,
274+
);
275+
sessionStorage.setItem(key, JSON.stringify(metadata));
276+
}
277+
278+
getServerMetadata(): OAuthMetadata | null {
279+
const key = getServerSpecificKey(
280+
SESSION_KEYS.SERVER_METADATA,
281+
this.serverUrl,
282+
);
283+
const metadata = sessionStorage.getItem(key);
284+
if (!metadata) {
285+
return null;
286+
}
287+
return JSON.parse(metadata);
288+
}
289+
290+
clear() {
291+
super.clear();
292+
sessionStorage.removeItem(
293+
getServerSpecificKey(SESSION_KEYS.SERVER_METADATA, this.serverUrl),
294+
);
295+
}
296+
}

0 commit comments

Comments
 (0)