Merge branch 'main' into main

This commit is contained in:
Balaj Marius 2025-05-27 16:10:00 +03:00 • committed by GitHub
commit 3ea9a19a76
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 80 additions and 35 deletions

View file

@ -1,11 +1,6 @@
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { DebugInspectorOAuthClientProvider } from "../lib/auth"; import { DebugInspectorOAuthClientProvider } from "../lib/auth";
import {
auth,
discoverOAuthMetadata,
} from "@modelcontextprotocol/sdk/client/auth.js";
import { OAuthMetadataSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
import { AlertCircle } from "lucide-react"; import { AlertCircle } from "lucide-react";
import { AuthDebuggerState } from "../lib/auth-types"; import { AuthDebuggerState } from "../lib/auth-types";
import { OAuthFlowProgress } from "./OAuthFlowProgress"; import { OAuthFlowProgress } from "./OAuthFlowProgress";
@ -124,22 +119,43 @@ const AuthDebugger = ({
updateAuthState({ isInitiatingAuth: true, statusMessage: null }); updateAuthState({ isInitiatingAuth: true, statusMessage: null });
try { try {
const serverAuthProvider = new DebugInspectorOAuthClientProvider( // Step through the OAuth flow using the state machine instead of the auth() function
serverUrl, let currentState: AuthDebuggerState = {
); ...authState,
// First discover OAuth metadata separately so we can save it oauthStep: "metadata_discovery",
const metadata = await discoverOAuthMetadata(serverUrl); authorizationUrl: null,
if (!metadata) { latestError: null,
throw new Error("Failed to discover OAuth metadata"); };
}
const parsedMetadata = await OAuthMetadataSchema.parseAsync(metadata);
serverAuthProvider.saveServerMetadata(parsedMetadata);
await auth(serverAuthProvider, { serverUrl: serverUrl }); const oauthMachine = new OAuthStateMachine(serverUrl, (updates) => {
// Update our temporary state during the process
currentState = { ...currentState, ...updates };
// But don't call updateAuthState yet
});
// Manually step through each stage of the OAuth flow
while (currentState.oauthStep !== "complete") {
await oauthMachine.executeStep(currentState);
// In quick mode, we'll just redirect to the authorization URL
if (
currentState.oauthStep === "authorization_code" &&
currentState.authorizationUrl
) {
// Open the authorization URL automatically
window.location.href = currentState.authorizationUrl;
break;
}
}
// After the flow completes or reaches a user-input step, update the app state
updateAuthState({ updateAuthState({
...currentState,
statusMessage: { statusMessage: {
type: "info", type: "info",
message: "Starting OAuth authentication process...", message:
currentState.oauthStep === "complete"
? "Authentication completed successfully"
: "Please complete authentication in the opened window and enter the code",
}, },
}); });
} catch (error) { } catch (error) {
@ -153,7 +169,7 @@ const AuthDebugger = ({
} finally { } finally {
updateAuthState({ isInitiatingAuth: false }); updateAuthState({ isInitiatingAuth: false });
} }
}, [serverUrl, updateAuthState]); }, [serverUrl, updateAuthState, authState]);
const handleClearOAuth = useCallback(() => { const handleClearOAuth = useCallback(() => {
if (serverUrl) { if (serverUrl) {

View file

@ -2,6 +2,8 @@ import { AuthDebuggerState, OAuthStep } from "@/lib/auth-types";
import { CheckCircle2, Circle, ExternalLink } from "lucide-react"; import { CheckCircle2, Circle, ExternalLink } from "lucide-react";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { DebugInspectorOAuthClientProvider } from "@/lib/auth"; import { DebugInspectorOAuthClientProvider } from "@/lib/auth";
import { useEffect, useMemo, useState } from "react";
import { OAuthClientInformation } from "@modelcontextprotocol/sdk/shared/auth.js";
interface OAuthStepProps { interface OAuthStepProps {
label: string; label: string;
@ -54,24 +56,57 @@ interface OAuthFlowProgressProps {
proceedToNextStep: () => Promise<void>; proceedToNextStep: () => Promise<void>;
} }
const steps: Array<OAuthStep> = [
"metadata_discovery",
"client_registration",
"authorization_redirect",
"authorization_code",
"token_request",
"complete",
];
export const OAuthFlowProgress = ({ export const OAuthFlowProgress = ({
serverUrl, serverUrl,
authState, authState,
updateAuthState, updateAuthState,
proceedToNextStep, proceedToNextStep,
}: OAuthFlowProgressProps) => { }: OAuthFlowProgressProps) => {
const provider = new DebugInspectorOAuthClientProvider(serverUrl); const provider = useMemo(
() => new DebugInspectorOAuthClientProvider(serverUrl),
[serverUrl],
);
const [clientInfo, setClientInfo] = useState<OAuthClientInformation | null>(
null,
);
const steps: Array<OAuthStep> = [
"metadata_discovery",
"client_registration",
"authorization_redirect",
"authorization_code",
"token_request",
"complete",
];
const currentStepIdx = steps.findIndex((s) => s === authState.oauthStep); const currentStepIdx = steps.findIndex((s) => s === authState.oauthStep);
useEffect(() => {
const fetchClientInfo = async () => {
if (authState.oauthClientInfo) {
setClientInfo(authState.oauthClientInfo);
} else {
try {
const info = await provider.clientInformation();
if (info) {
setClientInfo(info);
}
} catch (error) {
console.error("Failed to fetch client information:", error);
}
}
};
if (currentStepIdx > steps.indexOf("client_registration")) {
fetchClientInfo();
}
}, [
provider,
authState.oauthStep,
authState.oauthClientInfo,
currentStepIdx,
]);
// Helper to get step props // Helper to get step props
const getStepProps = (stepName: OAuthStep) => ({ const getStepProps = (stepName: OAuthStep) => ({
isComplete: isComplete:
@ -110,13 +145,13 @@ export const OAuthFlowProgress = ({
label="Client Registration" label="Client Registration"
{...getStepProps("client_registration")} {...getStepProps("client_registration")}
> >
{authState.oauthClientInfo && ( {clientInfo && (
<details className="text-xs mt-2"> <details className="text-xs mt-2">
<summary className="cursor-pointer text-muted-foreground font-medium"> <summary className="cursor-pointer text-muted-foreground font-medium">
Registered Client Information Registered Client Information
</summary> </summary>
<pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]"> <pre className="mt-2 p-2 bg-muted rounded-md overflow-auto max-h-[300px]">
{JSON.stringify(authState.oauthClientInfo, null, 2)} {JSON.stringify(clientInfo, null, 2)}
</pre> </pre>
</details> </details>
)} )}

View file

@ -207,16 +207,10 @@ describe("AuthDebugger", () => {
"https://example.com", "https://example.com",
); );
// Then should call auth with the server provider
expect(mockAuth).toHaveBeenCalled();
// Check that updateAuthState was called with the right info message // Check that updateAuthState was called with the right info message
expect(updateAuthState).toHaveBeenCalledWith( expect(updateAuthState).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
statusMessage: { oauthStep: "authorization_code",
type: "info",
message: "Starting OAuth authentication process...",
},
}), }),
); );
}); });