Skip to content

Commit 65ee7c0

Browse files
committed
Add support for OAuth via the proxy backend
1 parent 452d8dd commit 65ee7c0

File tree

16 files changed

+1026
-62
lines changed

16 files changed

+1026
-62
lines changed

client/src/App.tsx

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
2121
import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants";
2222
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types";
2323
import { OAuthStateMachine } from "./lib/oauth-state-machine";
24+
import {
25+
createOAuthProviderForServer,
26+
setOAuthMode,
27+
} from "./lib/oauth/provider-factory";
2428
import { cacheToolOutputSchemas } from "./utils/schemaUtils";
2529
import { cleanParams } from "./utils/paramUtils";
2630
import type { JsonSchemaType } from "./utils/jsonUtils";
@@ -145,6 +149,12 @@ const App = () => {
145149
return localStorage.getItem("lastOauthClientSecret") || "";
146150
});
147151

152+
const [oauthMode, setOauthMode] = useState<"direct" | "proxy">(() => {
153+
return (
154+
(localStorage.getItem("lastOauthMode") as "direct" | "proxy") || "direct"
155+
);
156+
});
157+
148158
// Custom headers state with migration from legacy auth
149159
const [customHeaders, setCustomHeaders] = useState<CustomHeaders>(() => {
150160
const savedHeaders = localStorage.getItem("lastCustomHeaders");
@@ -399,6 +409,18 @@ const App = () => {
399409
localStorage.setItem("lastOauthScope", oauthScope);
400410
}, [oauthScope]);
401411

412+
useEffect(() => {
413+
localStorage.setItem("lastOauthMode", oauthMode);
414+
}, [oauthMode]);
415+
416+
// Sync OAuth mode to sessionStorage when server URL changes
417+
useEffect(() => {
418+
if (sseUrl) {
419+
const key = getServerSpecificKey(SESSION_KEYS.OAUTH_MODE, sseUrl);
420+
sessionStorage.setItem(key, oauthMode);
421+
}
422+
}, [sseUrl, oauthMode]);
423+
402424
useEffect(() => {
403425
localStorage.setItem("lastOauthClientSecret", oauthClientSecret);
404426
}, [oauthClientSecret]);
@@ -446,9 +468,24 @@ const App = () => {
446468
};
447469

448470
try {
449-
const stateMachine = new OAuthStateMachine(sseUrl, (updates) => {
450-
currentState = { ...currentState, ...updates };
451-
});
471+
// Set the OAuth mode in sessionStorage before creating the provider
472+
setOAuthMode(oauthMode, sseUrl);
473+
474+
const proxyAddress = getMCPProxyAddress(config);
475+
const proxyAuthObj = getMCPProxyAuthToken(config);
476+
const oauthProvider = createOAuthProviderForServer(
477+
sseUrl,
478+
proxyAddress,
479+
proxyAuthObj.token,
480+
);
481+
482+
const stateMachine = new OAuthStateMachine(
483+
sseUrl,
484+
(updates) => {
485+
currentState = { ...currentState, ...updates };
486+
},
487+
oauthProvider,
488+
);
452489

453490
while (
454491
currentState.oauthStep !== "complete" &&
@@ -486,7 +523,7 @@ const App = () => {
486523
});
487524
}
488525
},
489-
[sseUrl],
526+
[sseUrl, oauthMode, config],
490527
);
491528

492529
useEffect(() => {
@@ -854,6 +891,8 @@ const App = () => {
854891
onBack={() => setIsAuthDebuggerVisible(false)}
855892
authState={authState}
856893
updateAuthState={updateAuthState}
894+
config={config}
895+
oauthMode={oauthMode}
857896
/>
858897
</TabsContent>
859898
);
@@ -913,6 +952,8 @@ const App = () => {
913952
setOauthClientSecret={setOauthClientSecret}
914953
oauthScope={oauthScope}
915954
setOauthScope={setOauthScope}
955+
oauthMode={oauthMode}
956+
setOauthMode={setOauthMode}
916957
onConnect={connectMcpServer}
917958
onDisconnect={disconnectMcpServer}
918959
logLevel={logLevel}

client/src/components/AuthDebugger.tsx

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,20 @@ import { OAuthFlowProgress } from "./OAuthFlowProgress";
77
import { OAuthStateMachine } from "../lib/oauth-state-machine";
88
import { SESSION_KEYS } from "../lib/constants";
99
import { validateRedirectUrl } from "@/utils/urlValidation";
10+
import {
11+
createOAuthProviderForServer,
12+
setOAuthMode,
13+
} from "../lib/oauth/provider-factory";
14+
import { InspectorConfig } from "../lib/configurationTypes";
15+
import { getMCPProxyAddress, getMCPProxyAuthToken } from "@/utils/configUtils";
1016

1117
export interface AuthDebuggerProps {
1218
serverUrl: string;
1319
onBack: () => void;
1420
authState: AuthDebuggerState;
1521
updateAuthState: (updates: Partial<AuthDebuggerState>) => void;
22+
config: InspectorConfig;
23+
oauthMode: "direct" | "proxy";
1624
}
1725

1826
interface StatusMessageProps {
@@ -60,7 +68,23 @@ const AuthDebugger = ({
6068
onBack,
6169
authState,
6270
updateAuthState,
71+
config,
72+
oauthMode,
6373
}: AuthDebuggerProps) => {
74+
// Create OAuth provider based on mode with proxy credentials
75+
const oauthProvider = useMemo(() => {
76+
// Set the OAuth mode in sessionStorage before creating the provider
77+
setOAuthMode(oauthMode, serverUrl);
78+
79+
const proxyAddress = getMCPProxyAddress(config);
80+
const proxyAuthObj = getMCPProxyAuthToken(config);
81+
return createOAuthProviderForServer(
82+
serverUrl,
83+
proxyAddress,
84+
proxyAuthObj.token,
85+
);
86+
}, [serverUrl, config, oauthMode]);
87+
6488
// Check for existing tokens on mount
6589
useEffect(() => {
6690
if (serverUrl && !authState.oauthTokens) {
@@ -103,8 +127,8 @@ const AuthDebugger = ({
103127
}, [serverUrl, updateAuthState]);
104128

105129
const stateMachine = useMemo(
106-
() => new OAuthStateMachine(serverUrl, updateAuthState),
107-
[serverUrl, updateAuthState],
130+
() => new OAuthStateMachine(serverUrl, updateAuthState, oauthProvider),
131+
[serverUrl, updateAuthState, oauthProvider],
108132
);
109133

110134
const proceedToNextStep = useCallback(async () => {
@@ -150,11 +174,15 @@ const AuthDebugger = ({
150174
latestError: null,
151175
};
152176

153-
const oauthMachine = new OAuthStateMachine(serverUrl, (updates) => {
154-
// Update our temporary state during the process
155-
currentState = { ...currentState, ...updates };
156-
// But don't call updateAuthState yet
157-
});
177+
const oauthMachine = new OAuthStateMachine(
178+
serverUrl,
179+
(updates) => {
180+
// Update our temporary state during the process
181+
currentState = { ...currentState, ...updates };
182+
// But don't call updateAuthState yet
183+
},
184+
oauthProvider,
185+
);
158186

159187
// Manually step through each stage of the OAuth flow
160188
while (currentState.oauthStep !== "complete") {

client/src/components/Sidebar.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ interface SidebarProps {
6262
setOauthClientSecret: (secret: string) => void;
6363
oauthScope: string;
6464
setOauthScope: (scope: string) => void;
65+
oauthMode: "direct" | "proxy";
66+
setOauthMode: (mode: "direct" | "proxy") => void;
6567
onConnect: () => void;
6668
onDisconnect: () => void;
6769
logLevel: LoggingLevel;
@@ -93,6 +95,8 @@ const Sidebar = ({
9395
setOauthClientSecret,
9496
oauthScope,
9597
setOauthScope,
98+
oauthMode,
99+
setOauthMode,
96100
onConnect,
97101
onDisconnect,
98102
logLevel,
@@ -552,6 +556,43 @@ const Sidebar = ({
552556
OAuth 2.0 Flow
553557
</h4>
554558
<div className="space-y-2">
559+
<div className="flex items-center justify-between">
560+
<label className="text-sm font-medium">
561+
OAuth Mode
562+
</label>
563+
<Tooltip>
564+
<TooltipTrigger asChild>
565+
<HelpCircle className="h-4 w-4 text-muted-foreground" />
566+
</TooltipTrigger>
567+
<TooltipContent className="max-w-xs">
568+
<p className="text-xs">
569+
<strong>Direct:</strong> Browser-based OAuth (may
570+
encounter CORS issues)
571+
<br />
572+
<strong>Via Proxy:</strong> Backend-proxied OAuth
573+
to avoid CORS
574+
</p>
575+
</TooltipContent>
576+
</Tooltip>
577+
</div>
578+
<Select
579+
value={oauthMode}
580+
onValueChange={(value: "direct" | "proxy") =>
581+
setOauthMode(value)
582+
}
583+
>
584+
<SelectTrigger data-testid="oauth-mode-select">
585+
<SelectValue placeholder="Select OAuth mode" />
586+
</SelectTrigger>
587+
<SelectContent>
588+
<SelectItem value="direct">
589+
Direct (browser-based)
590+
</SelectItem>
591+
<SelectItem value="proxy">
592+
Via Proxy (avoids CORS)
593+
</SelectItem>
594+
</SelectContent>
595+
</Select>
555596
<label className="text-sm font-medium">Client ID</label>
556597
<Input
557598
placeholder="Client ID"

client/src/components/__tests__/AuthDebugger.test.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import "@testing-library/jest-dom";
99
import { describe, it, beforeEach, jest } from "@jest/globals";
1010
import AuthDebugger, { AuthDebuggerProps } from "../AuthDebugger";
1111
import { TooltipProvider } from "@/components/ui/tooltip";
12-
import { SESSION_KEYS } from "@/lib/constants";
12+
import { SESSION_KEYS, DEFAULT_INSPECTOR_CONFIG } from "@/lib/constants";
1313

1414
const mockOAuthTokens = {
1515
access_token: "test_access_token",
@@ -149,6 +149,8 @@ describe("AuthDebugger", () => {
149149
onBack: jest.fn(),
150150
authState: defaultAuthState,
151151
updateAuthState: jest.fn(),
152+
config: DEFAULT_INSPECTOR_CONFIG,
153+
oauthMode: "direct" as const,
152154
};
153155

154156
beforeEach(() => {

client/src/components/__tests__/Sidebar.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ describe("Sidebar", () => {
4848
setOauthClientSecret: jest.fn(),
4949
oauthScope: "",
5050
setOauthScope: jest.fn(),
51+
oauthMode: "direct" as const,
52+
setOauthMode: jest.fn(),
5153
env: {},
5254
setEnv: jest.fn(),
5355
customHeaders: [],

client/src/lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const SESSION_KEYS = {
1818
SERVER_METADATA: "mcp_server_metadata",
1919
AUTH_DEBUGGER_STATE: "mcp_auth_debugger_state",
2020
SCOPE: "mcp_scope",
21+
OAUTH_MODE: "mcp_oauth_mode",
2122
} as const;
2223

2324
// Generate server-specific session storage keys

0 commit comments

Comments
 (0)