Skip to content

Commit b3f7139

Browse files
committed
WIP
1 parent 65ee7c0 commit b3f7139

19 files changed

+944
-250
lines changed

client/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ const App = () => {
523523
});
524524
}
525525
},
526-
[sseUrl, oauthMode, config],
526+
[sseUrl, oauthMode, config, connectMcpServer],
527527
);
528528

529529
useEffect(() => {

client/src/components/OAuthCallback.tsx

Lines changed: 97 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ import {
77
generateOAuthErrorDescription,
88
parseOAuthCallbackParams,
99
} from "@/utils/oauthUtils.ts";
10+
import { createOAuthProviderForServer } from "../lib/oauth/provider-factory";
11+
import { OAuthStateMachine } from "../lib/oauth-state-machine";
12+
import { AuthDebuggerState } from "../lib/auth-types";
13+
import {
14+
getMCPProxyAddress,
15+
getMCPProxyAuthToken,
16+
initializeInspectorConfig,
17+
} from "@/utils/configUtils";
1018

1119
interface OAuthCallbackProps {
1220
onConnect: (serverUrl: string) => void;
@@ -41,24 +49,97 @@ const OAuthCallback = ({ onConnect }: OAuthCallbackProps) => {
4149
return notifyError("Missing Server URL");
4250
}
4351

44-
let result;
45-
try {
46-
// Create an auth provider with the current server URL
47-
const serverAuthProvider = new InspectorOAuthClientProvider(serverUrl);
52+
// Check if there's stored auth state (for proxy mode from Connect button)
53+
const storedAuthState = sessionStorage.getItem(
54+
SESSION_KEYS.AUTH_STATE_FOR_CONNECT,
55+
);
4856

49-
result = await auth(serverAuthProvider, {
50-
serverUrl,
51-
authorizationCode: params.code,
52-
});
53-
} catch (error) {
54-
console.error("OAuth callback error:", error);
55-
return notifyError(`Unexpected error occurred: ${error}`);
56-
}
57+
if (storedAuthState) {
58+
// Proxy mode: Complete the OAuth flow using the state machine
59+
try {
60+
let restoredState: AuthDebuggerState = JSON.parse(storedAuthState);
61+
62+
// Restore URL objects
63+
if (
64+
restoredState.resource &&
65+
typeof restoredState.resource === "string"
66+
) {
67+
restoredState.resource = new URL(restoredState.resource);
68+
}
69+
if (
70+
restoredState.authorizationUrl &&
71+
typeof restoredState.authorizationUrl === "string"
72+
) {
73+
restoredState.authorizationUrl = new URL(
74+
restoredState.authorizationUrl,
75+
);
76+
}
77+
78+
// Set up state with the authorization code
79+
let currentState: AuthDebuggerState = {
80+
...restoredState,
81+
authorizationCode: params.code,
82+
oauthStep: "token_request",
83+
};
84+
85+
// Get config and create provider
86+
// Use the same config key and initialization as App.tsx
87+
const config = initializeInspectorConfig("inspectorConfig_v1");
88+
89+
const proxyAddress = getMCPProxyAddress(config);
90+
const proxyAuthObj = getMCPProxyAuthToken(config);
91+
92+
const oauthProvider = createOAuthProviderForServer(
93+
serverUrl,
94+
proxyAddress,
95+
proxyAuthObj.token,
96+
);
97+
98+
const stateMachine = new OAuthStateMachine(
99+
serverUrl,
100+
(updates) => {
101+
currentState = { ...currentState, ...updates };
102+
},
103+
oauthProvider,
104+
false, // use regular redirect URL
105+
);
106+
107+
// Complete the token exchange
108+
await stateMachine.executeStep(currentState);
109+
110+
if (currentState.oauthStep !== "complete") {
111+
return notifyError("Failed to complete OAuth token exchange");
112+
}
113+
114+
// Clean up stored state
115+
sessionStorage.removeItem(SESSION_KEYS.AUTH_STATE_FOR_CONNECT);
116+
} catch (error) {
117+
console.error("Proxy OAuth callback error:", error);
118+
sessionStorage.removeItem(SESSION_KEYS.AUTH_STATE_FOR_CONNECT);
119+
return notifyError(`Failed to complete proxy OAuth: ${error}`);
120+
}
121+
} else {
122+
// Direct mode: Use SDK's auth() function
123+
let result;
124+
try {
125+
const serverAuthProvider = new InspectorOAuthClientProvider(
126+
serverUrl,
127+
);
128+
129+
result = await auth(serverAuthProvider, {
130+
serverUrl,
131+
authorizationCode: params.code,
132+
});
133+
} catch (error) {
134+
console.error("OAuth callback error:", error);
135+
return notifyError(`Unexpected error occurred: ${error}`);
136+
}
57137

58-
if (result !== "AUTHORIZED") {
59-
return notifyError(
60-
`Expected to be authorized after providing auth code, got: ${result}`,
61-
);
138+
if (result !== "AUTHORIZED") {
139+
return notifyError(
140+
`Expected to be authorized after providing auth code, got: ${result}`,
141+
);
142+
}
62143
}
63144

64145
// Finally, trigger auto-connect

client/src/lib/auth.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -254,10 +254,17 @@ export class InspectorOAuthClientProvider implements OAuthClientProvider {
254254
// Overrides redirect URL to use the debug endpoint and allows saving server OAuth metadata to
255255
// display in debug UI.
256256
export class DebugInspectorOAuthClientProvider extends InspectorOAuthClientProvider {
257+
constructor(
258+
serverUrl: string,
259+
private useDebugRedirect: boolean = true,
260+
) {
261+
super(serverUrl);
262+
}
263+
257264
get redirectUrl(): string {
258-
// We can use the debug redirect URL here because it was already registered
259-
// in the parent class's clientMetadata along with the normal redirect URL
260-
return this.debugRedirectUrl;
265+
// Use debug redirect URL by default, or regular redirect URL when configured
266+
// (e.g., when Connect button is clicked in proxy mode)
267+
return this.useDebugRedirect ? this.debugRedirectUrl : super.redirectUrl;
261268
}
262269

263270
saveServerMetadata(metadata: OAuthMetadata) {

client/src/lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const SESSION_KEYS = {
1717
PREREGISTERED_CLIENT_INFORMATION: "mcp_preregistered_client_information",
1818
SERVER_METADATA: "mcp_server_metadata",
1919
AUTH_DEBUGGER_STATE: "mcp_auth_debugger_state",
20+
AUTH_STATE_FOR_CONNECT: "mcp_auth_state_for_connect",
2021
SCOPE: "mcp_scope",
2122
OAUTH_MODE: "mcp_oauth_mode",
2223
} as const;

client/src/lib/hooks/useConnection.ts

Lines changed: 137 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";
3535
import { useEffect, useState } from "react";
3636
import { useToast } from "@/lib/hooks/useToast";
3737
import { z } from "zod";
38-
import { ConnectionStatus, CLIENT_IDENTITY } from "../constants";
38+
import { ConnectionStatus, CLIENT_IDENTITY, SESSION_KEYS } from "../constants";
3939
import { Notification } from "../notificationTypes";
4040
import {
4141
auth,
@@ -47,16 +47,22 @@ import {
4747
saveClientInformationToSessionStorage,
4848
saveScopeToSessionStorage,
4949
clearScopeFromSessionStorage,
50-
discoverScopes,
5150
} from "../auth";
5251
import {
5352
getMCPProxyAddress,
5453
getMCPServerRequestMaxTotalTimeout,
5554
resetRequestTimeoutOnProgress,
5655
getMCPProxyAuthToken,
56+
getMCPServerRequestTimeout,
5757
} from "@/utils/configUtils";
58-
import { getMCPServerRequestTimeout } from "@/utils/configUtils";
5958
import { InspectorConfig } from "../configurationTypes";
59+
import {
60+
createOAuthProviderForServer,
61+
setOAuthMode,
62+
} from "../oauth/provider-factory";
63+
import { OAuthStateMachine } from "../oauth-state-machine";
64+
import { AuthDebuggerState } from "../auth-types";
65+
import { validateRedirectUrl } from "@/utils/urlValidation";
6066
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
6167
import { CustomHeaders } from "../types/customHeaders";
6268

@@ -153,6 +159,13 @@ export function useConnection({
153159
saveScopeToSessionStorage(sseUrl, oauthScope);
154160
}, [oauthScope, sseUrl]);
155161

162+
// Sync OAuth mode with connection type
163+
useEffect(() => {
164+
// When connection type is set to proxy, ensure OAuth mode is also proxy
165+
// When connection type is direct, OAuth mode should be direct
166+
setOAuthMode(connectionType, sseUrl);
167+
}, [connectionType, sseUrl]);
168+
156169
const pushHistory = (request: object, response?: object) => {
157170
setRequestHistory((prev) => [
158171
...prev,
@@ -344,28 +357,131 @@ export function useConnection({
344357

345358
const handleAuthError = async (error: unknown) => {
346359
if (is401Error(error)) {
347-
let scope = oauthScope?.trim();
348-
if (!scope) {
349-
// Only discover resource metadata when we need to discover scopes
350-
let resourceMetadata;
351-
try {
352-
resourceMetadata = await discoverOAuthProtectedResourceMetadata(
353-
new URL("/", sseUrl),
360+
const scope = oauthScope?.trim();
361+
362+
// Use connectionType directly instead of reading from session storage
363+
const oauthMode = connectionType;
364+
365+
if (scope) {
366+
saveScopeToSessionStorage(sseUrl, scope);
367+
}
368+
369+
if (oauthMode === "proxy") {
370+
// Use proxy mode with state machine approach (to avoid CORS)
371+
// Ensure OAuth mode is set in session storage for callback to use
372+
setOAuthMode("proxy", sseUrl);
373+
374+
const proxyAddress = getMCPProxyAddress(config);
375+
const proxyAuthObj = getMCPProxyAuthToken(config);
376+
const oauthProvider = createOAuthProviderForServer(
377+
sseUrl,
378+
proxyAddress,
379+
proxyAuthObj.token,
380+
);
381+
382+
// Use state machine to step through OAuth flow
383+
let currentState: AuthDebuggerState = {
384+
oauthStep: "metadata_discovery",
385+
authorizationUrl: null,
386+
authorizationCode: "",
387+
oauthMetadata: null,
388+
oauthClientInfo: null,
389+
oauthTokens: null,
390+
resourceMetadata: null,
391+
resourceMetadataError: null,
392+
authServerUrl: null,
393+
resource: null,
394+
validationError: null,
395+
latestError: null,
396+
statusMessage: null,
397+
isInitiatingAuth: false,
398+
};
399+
400+
// Use regular redirect URL (not debug) for Connect button
401+
// This will redirect to /oauth/callback which auto-connects
402+
const oauthMachine = new OAuthStateMachine(
403+
sseUrl,
404+
(updates) => {
405+
currentState = { ...currentState, ...updates };
406+
},
407+
oauthProvider,
408+
false, // useDebugRedirect = false
409+
);
410+
411+
// Step through OAuth flow until we need to redirect
412+
while (currentState.oauthStep !== "complete") {
413+
await oauthMachine.executeStep(currentState);
414+
415+
// When we reach authorization step, validate and redirect
416+
if (
417+
currentState.oauthStep === "authorization_code" &&
418+
currentState.authorizationUrl
419+
) {
420+
try {
421+
validateRedirectUrl(currentState.authorizationUrl);
422+
} catch (redirectError) {
423+
console.error("Invalid authorization URL:", redirectError);
424+
return false;
425+
}
426+
427+
// Store the current auth state before redirecting
428+
// This allows /oauth/callback to complete the token exchange via proxy
429+
sessionStorage.setItem(
430+
SESSION_KEYS.AUTH_STATE_FOR_CONNECT,
431+
JSON.stringify(currentState),
432+
);
433+
434+
// Redirect to authorization URL
435+
// Will redirect to /oauth/callback which calls onOAuthConnect
436+
// and auto-connects after OAuth completion
437+
window.location.href = currentState.authorizationUrl.toString();
438+
return false; // We're redirecting, so connection will be retried after OAuth
439+
}
440+
}
441+
442+
// If we completed the full flow (shouldn't happen on first connect)
443+
return currentState.oauthStep === "complete";
444+
} else {
445+
// Direct mode: Use SDK's auth() function
446+
let discoveredScope = scope;
447+
448+
// Discover scopes if not provided
449+
if (!discoveredScope) {
450+
const proxyAddress = getMCPProxyAddress(config);
451+
const proxyAuthObj = getMCPProxyAuthToken(config);
452+
const oauthProvider = createOAuthProviderForServer(
453+
sseUrl,
454+
proxyAddress,
455+
proxyAuthObj.token,
456+
);
457+
458+
// For direct mode, try to get resource metadata
459+
let resourceMetadata;
460+
try {
461+
resourceMetadata = await discoverOAuthProtectedResourceMetadata(
462+
new URL("/", sseUrl),
463+
);
464+
} catch {
465+
// Resource metadata is optional, continue without it
466+
}
467+
468+
discoveredScope = await oauthProvider.discoverScopes(
469+
sseUrl,
470+
resourceMetadata,
354471
);
355-
} catch {
356-
// Resource metadata is optional, continue without it
472+
if (discoveredScope) {
473+
saveScopeToSessionStorage(sseUrl, discoveredScope);
474+
}
357475
}
358-
scope = await discoverScopes(sseUrl, resourceMetadata);
359-
}
360476

361-
saveScopeToSessionStorage(sseUrl, scope);
362-
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
477+
const serverAuthProvider = new InspectorOAuthClientProvider(sseUrl);
363478

364-
const result = await auth(serverAuthProvider, {
365-
serverUrl: sseUrl,
366-
scope,
367-
});
368-
return result === "AUTHORIZED";
479+
const result = await auth(serverAuthProvider, {
480+
serverUrl: sseUrl,
481+
scope: discoveredScope,
482+
});
483+
return result === "AUTHORIZED";
484+
}
369485
}
370486

371487
return false;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Hook to get proxy configuration from config
3+
*/
4+
import { useMemo } from "react";
5+
import { InspectorConfig } from "../configurationTypes";
6+
import { getMCPProxyAddress, getMCPProxyAuthToken } from "@/utils/configUtils";
7+
8+
// This hook can be used if we need to get config from context
9+
// For now, it's simpler to just pass the config values directly
10+
export function useProxyConfig(config?: InspectorConfig) {
11+
const proxyFullAddress = useMemo(
12+
() => (config ? getMCPProxyAddress(config) : ""),
13+
[config],
14+
);
15+
16+
const proxyAuthToken = useMemo(
17+
() => (config ? getMCPProxyAuthToken(config) : ""),
18+
[config],
19+
);
20+
21+
return { proxyFullAddress, proxyAuthToken };
22+
}

0 commit comments

Comments
 (0)