Skip to content
Draft
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
49 changes: 45 additions & 4 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants";
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types";
import { OAuthStateMachine } from "./lib/oauth-state-machine";
import {
createOAuthProviderForServer,
setOAuthMode,
} from "./lib/oauth/provider-factory";
import { cacheToolOutputSchemas } from "./utils/schemaUtils";
import { cleanParams } from "./utils/paramUtils";
import type { JsonSchemaType } from "./utils/jsonUtils";
Expand Down Expand Up @@ -145,6 +149,12 @@ const App = () => {
return localStorage.getItem("lastOauthClientSecret") || "";
});

const [oauthMode, setOauthMode] = useState<"direct" | "proxy">(() => {
return (
(localStorage.getItem("lastOauthMode") as "direct" | "proxy") || "direct"
);
});

// Custom headers state with migration from legacy auth
const [customHeaders, setCustomHeaders] = useState<CustomHeaders>(() => {
const savedHeaders = localStorage.getItem("lastCustomHeaders");
Expand Down Expand Up @@ -399,6 +409,18 @@ const App = () => {
localStorage.setItem("lastOauthScope", oauthScope);
}, [oauthScope]);

useEffect(() => {
localStorage.setItem("lastOauthMode", oauthMode);
}, [oauthMode]);

// Sync OAuth mode to sessionStorage when server URL changes
useEffect(() => {
if (sseUrl) {
const key = getServerSpecificKey(SESSION_KEYS.OAUTH_MODE, sseUrl);
sessionStorage.setItem(key, oauthMode);
}
}, [sseUrl, oauthMode]);

useEffect(() => {
localStorage.setItem("lastOauthClientSecret", oauthClientSecret);
}, [oauthClientSecret]);
Expand Down Expand Up @@ -446,9 +468,24 @@ const App = () => {
};

try {
const stateMachine = new OAuthStateMachine(sseUrl, (updates) => {
currentState = { ...currentState, ...updates };
});
// Set the OAuth mode in sessionStorage before creating the provider
setOAuthMode(oauthMode, sseUrl);

const proxyAddress = getMCPProxyAddress(config);
const proxyAuthObj = getMCPProxyAuthToken(config);
const oauthProvider = createOAuthProviderForServer(
sseUrl,
proxyAddress,
proxyAuthObj.token,
);

const stateMachine = new OAuthStateMachine(
sseUrl,
(updates) => {
currentState = { ...currentState, ...updates };
},
oauthProvider,
);

while (
currentState.oauthStep !== "complete" &&
Expand Down Expand Up @@ -486,7 +523,7 @@ const App = () => {
});
}
},
[sseUrl],
[sseUrl, oauthMode, config, connectMcpServer],
);

useEffect(() => {
Expand Down Expand Up @@ -854,6 +891,8 @@ const App = () => {
onBack={() => setIsAuthDebuggerVisible(false)}
authState={authState}
updateAuthState={updateAuthState}
config={config}
oauthMode={oauthMode}
/>
</TabsContent>
);
Expand Down Expand Up @@ -913,6 +952,8 @@ const App = () => {
setOauthClientSecret={setOauthClientSecret}
oauthScope={oauthScope}
setOauthScope={setOauthScope}
oauthMode={oauthMode}
setOauthMode={setOauthMode}
onConnect={connectMcpServer}
onDisconnect={disconnectMcpServer}
logLevel={logLevel}
Expand Down
42 changes: 35 additions & 7 deletions client/src/components/AuthDebugger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,20 @@ import { OAuthFlowProgress } from "./OAuthFlowProgress";
import { OAuthStateMachine } from "../lib/oauth-state-machine";
import { SESSION_KEYS } from "../lib/constants";
import { validateRedirectUrl } from "@/utils/urlValidation";
import {
createOAuthProviderForServer,
setOAuthMode,
} from "../lib/oauth/provider-factory";
import { InspectorConfig } from "../lib/configurationTypes";
import { getMCPProxyAddress, getMCPProxyAuthToken } from "@/utils/configUtils";

export interface AuthDebuggerProps {
serverUrl: string;
onBack: () => void;
authState: AuthDebuggerState;
updateAuthState: (updates: Partial<AuthDebuggerState>) => void;
config: InspectorConfig;
oauthMode: "direct" | "proxy";
}

interface StatusMessageProps {
Expand Down Expand Up @@ -60,7 +68,23 @@ const AuthDebugger = ({
onBack,
authState,
updateAuthState,
config,
oauthMode,
}: AuthDebuggerProps) => {
// Create OAuth provider based on mode with proxy credentials
const oauthProvider = useMemo(() => {
// Set the OAuth mode in sessionStorage before creating the provider
setOAuthMode(oauthMode, serverUrl);

const proxyAddress = getMCPProxyAddress(config);
const proxyAuthObj = getMCPProxyAuthToken(config);
return createOAuthProviderForServer(
serverUrl,
proxyAddress,
proxyAuthObj.token,
);
}, [serverUrl, config, oauthMode]);

// Check for existing tokens on mount
useEffect(() => {
if (serverUrl && !authState.oauthTokens) {
Expand Down Expand Up @@ -103,8 +127,8 @@ const AuthDebugger = ({
}, [serverUrl, updateAuthState]);

const stateMachine = useMemo(
() => new OAuthStateMachine(serverUrl, updateAuthState),
[serverUrl, updateAuthState],
() => new OAuthStateMachine(serverUrl, updateAuthState, oauthProvider),
[serverUrl, updateAuthState, oauthProvider],
);

const proceedToNextStep = useCallback(async () => {
Expand Down Expand Up @@ -150,11 +174,15 @@ const AuthDebugger = ({
latestError: null,
};

const oauthMachine = new OAuthStateMachine(serverUrl, (updates) => {
// Update our temporary state during the process
currentState = { ...currentState, ...updates };
// But don't call updateAuthState yet
});
const oauthMachine = new OAuthStateMachine(
serverUrl,
(updates) => {
// Update our temporary state during the process
currentState = { ...currentState, ...updates };
// But don't call updateAuthState yet
},
oauthProvider,
);

// Manually step through each stage of the OAuth flow
while (currentState.oauthStep !== "complete") {
Expand Down
113 changes: 97 additions & 16 deletions client/src/components/OAuthCallback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import {
generateOAuthErrorDescription,
parseOAuthCallbackParams,
} from "@/utils/oauthUtils.ts";
import { createOAuthProviderForServer } from "../lib/oauth/provider-factory";
import { OAuthStateMachine } from "../lib/oauth-state-machine";
import { AuthDebuggerState } from "../lib/auth-types";
import {
getMCPProxyAddress,
getMCPProxyAuthToken,
initializeInspectorConfig,
} from "@/utils/configUtils";

interface OAuthCallbackProps {
onConnect: (serverUrl: string) => void;
Expand Down Expand Up @@ -41,24 +49,97 @@ const OAuthCallback = ({ onConnect }: OAuthCallbackProps) => {
return notifyError("Missing Server URL");
}

let result;
try {
// Create an auth provider with the current server URL
const serverAuthProvider = new InspectorOAuthClientProvider(serverUrl);
// Check if there's stored auth state (for proxy mode from Connect button)
const storedAuthState = sessionStorage.getItem(
SESSION_KEYS.AUTH_STATE_FOR_CONNECT,
);

result = await auth(serverAuthProvider, {
serverUrl,
authorizationCode: params.code,
});
} catch (error) {
console.error("OAuth callback error:", error);
return notifyError(`Unexpected error occurred: ${error}`);
}
if (storedAuthState) {
// Proxy mode: Complete the OAuth flow using the state machine
try {
let restoredState: AuthDebuggerState = JSON.parse(storedAuthState);

// Restore URL objects
if (
restoredState.resource &&
typeof restoredState.resource === "string"
) {
restoredState.resource = new URL(restoredState.resource);
}
if (
restoredState.authorizationUrl &&
typeof restoredState.authorizationUrl === "string"
) {
restoredState.authorizationUrl = new URL(
restoredState.authorizationUrl,
);
}

// Set up state with the authorization code
let currentState: AuthDebuggerState = {
...restoredState,
authorizationCode: params.code,
oauthStep: "token_request",
};

// Get config and create provider
// Use the same config key and initialization as App.tsx
const config = initializeInspectorConfig("inspectorConfig_v1");

const proxyAddress = getMCPProxyAddress(config);
const proxyAuthObj = getMCPProxyAuthToken(config);

const oauthProvider = createOAuthProviderForServer(
serverUrl,
proxyAddress,
proxyAuthObj.token,
);

const stateMachine = new OAuthStateMachine(
serverUrl,
(updates) => {
currentState = { ...currentState, ...updates };
},
oauthProvider,
false, // use regular redirect URL
);

// Complete the token exchange
await stateMachine.executeStep(currentState);

if (currentState.oauthStep !== "complete") {
return notifyError("Failed to complete OAuth token exchange");
}

// Clean up stored state
sessionStorage.removeItem(SESSION_KEYS.AUTH_STATE_FOR_CONNECT);
} catch (error) {
console.error("Proxy OAuth callback error:", error);
sessionStorage.removeItem(SESSION_KEYS.AUTH_STATE_FOR_CONNECT);
return notifyError(`Failed to complete proxy OAuth: ${error}`);
}
} else {
// Direct mode: Use SDK's auth() function
let result;
try {
const serverAuthProvider = new InspectorOAuthClientProvider(
serverUrl,
);

result = await auth(serverAuthProvider, {
serverUrl,
authorizationCode: params.code,
});
} catch (error) {
console.error("OAuth callback error:", error);
return notifyError(`Unexpected error occurred: ${error}`);
}

if (result !== "AUTHORIZED") {
return notifyError(
`Expected to be authorized after providing auth code, got: ${result}`,
);
if (result !== "AUTHORIZED") {
return notifyError(
`Expected to be authorized after providing auth code, got: ${result}`,
);
}
}

// Finally, trigger auto-connect
Expand Down
41 changes: 41 additions & 0 deletions client/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ interface SidebarProps {
setOauthClientSecret: (secret: string) => void;
oauthScope: string;
setOauthScope: (scope: string) => void;
oauthMode: "direct" | "proxy";
setOauthMode: (mode: "direct" | "proxy") => void;
onConnect: () => void;
onDisconnect: () => void;
logLevel: LoggingLevel;
Expand Down Expand Up @@ -93,6 +95,8 @@ const Sidebar = ({
setOauthClientSecret,
oauthScope,
setOauthScope,
oauthMode,
setOauthMode,
onConnect,
onDisconnect,
logLevel,
Expand Down Expand Up @@ -552,6 +556,43 @@ const Sidebar = ({
OAuth 2.0 Flow
</h4>
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-sm font-medium">
OAuth Mode
</label>
<Tooltip>
<TooltipTrigger asChild>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">
<p className="text-xs">
<strong>Direct:</strong> Browser-based OAuth (may
encounter CORS issues)
<br />
<strong>Via Proxy:</strong> Backend-proxied OAuth
to avoid CORS
</p>
</TooltipContent>
</Tooltip>
</div>
<Select
value={oauthMode}
onValueChange={(value: "direct" | "proxy") =>
setOauthMode(value)
}
>
<SelectTrigger data-testid="oauth-mode-select">
<SelectValue placeholder="Select OAuth mode" />
</SelectTrigger>
<SelectContent>
<SelectItem value="direct">
Direct (browser-based)
</SelectItem>
<SelectItem value="proxy">
Via Proxy (avoids CORS)
</SelectItem>
</SelectContent>
</Select>
<label className="text-sm font-medium">Client ID</label>
<Input
placeholder="Client ID"
Expand Down
4 changes: 3 additions & 1 deletion client/src/components/__tests__/AuthDebugger.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import "@testing-library/jest-dom";
import { describe, it, beforeEach, jest } from "@jest/globals";
import AuthDebugger, { AuthDebuggerProps } from "../AuthDebugger";
import { TooltipProvider } from "@/components/ui/tooltip";
import { SESSION_KEYS } from "@/lib/constants";
import { SESSION_KEYS, DEFAULT_INSPECTOR_CONFIG } from "@/lib/constants";

const mockOAuthTokens = {
access_token: "test_access_token",
Expand Down Expand Up @@ -149,6 +149,8 @@ describe("AuthDebugger", () => {
onBack: jest.fn(),
authState: defaultAuthState,
updateAuthState: jest.fn(),
config: DEFAULT_INSPECTOR_CONFIG,
oauthMode: "direct" as const,
};

beforeEach(() => {
Expand Down
2 changes: 2 additions & 0 deletions client/src/components/__tests__/Sidebar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ describe("Sidebar", () => {
setOauthClientSecret: jest.fn(),
oauthScope: "",
setOauthScope: jest.fn(),
oauthMode: "direct" as const,
setOauthMode: jest.fn(),
env: {},
setEnv: jest.fn(),
customHeaders: [],
Expand Down
Loading
Loading