Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
93 changes: 31 additions & 62 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@
isReservedMetaKey,
} from "@/utils/metaUtils";
import { AuthDebuggerState, EMPTY_DEBUGGER_STATE } from "./lib/auth-types";
import { OAuthStateMachine } from "./lib/oauth-state-machine";
import { cacheToolOutputSchemas } from "./utils/schemaUtils";
import { cleanParams } from "./utils/paramUtils";
import type { JsonSchemaType } from "./utils/jsonUtils";
import React, {
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
Expand Down Expand Up @@ -251,7 +251,7 @@
return {};
});

const updateAuthState = (updates: Partial<AuthDebuggerState>) => {

Check warning on line 254 in client/src/App.tsx

View workflow job for this annotation

GitHub Actions / build

The 'updateAuthState' function makes the dependencies of useMemo Hook (at line 1157) change on every render. To fix this, wrap the definition of 'updateAuthState' in its own useCallback() Hook
setAuthState((prev) => ({ ...prev, ...updates }));
};

Expand Down Expand Up @@ -552,10 +552,9 @@
);

const onOAuthDebugConnect = useCallback(
async ({
({
authorizationCode,
errorMsg,
restoredState,
}: {
authorizationCode?: string;
errorMsg?: string;
Expand All @@ -566,62 +565,26 @@
if (errorMsg) {
updateAuthState({
latestError: new Error(errorMsg),
statusMessage: {
type: "error",
message: errorMsg,
},
});
return;
}

if (restoredState && authorizationCode) {
let currentState: AuthDebuggerState = {
...restoredState,
authorizationCode,
oauthStep: "token_request",
isInitiatingAuth: true,
statusMessage: null,
latestError: null,
};

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

while (
currentState.oauthStep !== "complete" &&
currentState.oauthStep !== "authorization_code"
) {
await stateMachine.executeStep(currentState);
}

if (currentState.oauthStep === "complete") {
updateAuthState({
...currentState,
statusMessage: {
type: "success",
message: "Authentication completed successfully",
},
isInitiatingAuth: false,
});
}
} catch (error) {
console.error("OAuth continuation error:", error);
updateAuthState({
latestError:
error instanceof Error ? error : new Error(String(error)),
statusMessage: {
type: "error",
message: `Failed to complete OAuth flow: ${error instanceof Error ? error.message : String(error)}`,
},
isInitiatingAuth: false,
});
}
} else if (authorizationCode) {
if (authorizationCode) {
// Show info message - the user should use the debug flow to complete
updateAuthState({
authorizationCode,
oauthStep: "token_request",
statusMessage: {
type: "info",
message:
"Authorization code received. Use the debug flow to complete authentication.",
},
});
}
},
[sseUrl],
[],
);

useEffect(() => {
Expand Down Expand Up @@ -1177,15 +1140,21 @@
setLogLevel(level);
};

const AuthDebuggerWrapper = () => (
<TabsContent value="auth">
<AuthDebugger
serverUrl={sseUrl}
onBack={() => setIsAuthDebuggerVisible(false)}
authState={authState}
updateAuthState={updateAuthState}
/>
</TabsContent>
// Memoize the AuthDebugger to prevent re-mounting when App re-renders
// This is important because AuthDebugger has local state (showDebugFlow)
// that would be lost if the component was re-mounted.
const authDebuggerElement = useMemo(
() => (
<TabsContent value="auth">
<AuthDebugger
serverUrl={sseUrl}
onBack={() => setIsAuthDebuggerVisible(false)}
authState={authState}
updateAuthState={updateAuthState}
/>
</TabsContent>
),
[sseUrl, authState, updateAuthState],
);

if (window.location.pathname === "/oauth/callback") {
Expand Down Expand Up @@ -1522,7 +1491,7 @@
setRoots={setRoots}
onRootsChange={handleRootsChange}
/>
<AuthDebuggerWrapper />
{authDebuggerElement}
<MetadataTab
metadata={metadata}
onMetadataChange={handleMetadataChange}
Expand All @@ -1537,7 +1506,7 @@
className="w-full p-4"
onValueChange={(value) => (window.location.hash = value)}
>
<AuthDebuggerWrapper />
{authDebuggerElement}
</Tabs>
) : (
<div className="flex flex-col items-center justify-center h-full gap-4">
Expand Down
4 changes: 0 additions & 4 deletions client/src/__tests__/App.config.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
auth: jest.fn(),
}));

jest.mock("../lib/oauth-state-machine", () => ({
OAuthStateMachine: jest.fn(),
}));

jest.mock("../lib/auth", () => ({
InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
tokens: jest.fn().mockResolvedValue(null),
Expand Down
4 changes: 0 additions & 4 deletions client/src/__tests__/App.routing.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
auth: jest.fn(),
}));

jest.mock("../lib/oauth-state-machine", () => ({
OAuthStateMachine: jest.fn(),
}));

jest.mock("../lib/auth", () => ({
InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
tokens: jest.fn().mockResolvedValue(null),
Expand Down
4 changes: 0 additions & 4 deletions client/src/__tests__/App.samplingNavigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
auth: jest.fn(),
}));

jest.mock("../lib/oauth-state-machine", () => ({
OAuthStateMachine: jest.fn(),
}));

jest.mock("../lib/auth", () => ({
InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
tokens: jest.fn().mockResolvedValue(null),
Expand Down
Loading
Loading