Add Auth debugger tab (#355)

* wip auth debugger

* cleanup types and validation

* more cleanup

* draft test

* wip clean up some

* rm toasts

* consolidate state management

* prettier

* hoist state up to App

* working with quick and guided

* sort out displaying debugger

* prettier

* cleanup types

* fix tests

* cleanup comment

* prettier

* fixup types in tests

* prettier

* refactor debug to avoid toasting

* callback shuffling

* linting

* types

* rm toast in test

* bump typescript sdk version to 0.11.2 for scope parameter passing

* use proper scope handling

* test scope parameter passing

* move functions and s/sseUrl/serverUrl/

* extract status message into component

* refactor progress and steps into components

* fix test

* rename quick handler

* one less click

* last step complete

* add state machine

* test and types
This commit is contained in:
Paul Carleton
2025-05-13 19:37:09 +01:00
committed by GitHub
parent ad39ec27e7
commit 5e8e78c31d
11 changed files with 1391 additions and 13 deletions

View File

@@ -17,6 +17,9 @@ import {
Tool,
LoggingLevel,
} from "@modelcontextprotocol/sdk/types.js";
import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
import { SESSION_KEYS, getServerSpecificKey } from "./lib/constants";
import { AuthDebuggerState } from "./lib/auth-types";
import React, {
Suspense,
useCallback,
@@ -28,18 +31,21 @@ import { useConnection } from "./lib/hooks/useConnection";
import { useDraggablePane } from "./lib/hooks/useDraggablePane";
import { StdErrNotification } from "./lib/notificationTypes";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import {
Bell,
Files,
FolderTree,
Hammer,
Hash,
Key,
MessageSquare,
} from "lucide-react";
import { z } from "zod";
import "./App.css";
import AuthDebugger from "./components/AuthDebugger";
import ConsoleTab from "./components/ConsoleTab";
import HistoryAndNotifications from "./components/History";
import PingTab from "./components/PingTab";
@@ -111,6 +117,27 @@ const App = () => {
}
>
>([]);
const [isAuthDebuggerVisible, setIsAuthDebuggerVisible] = useState(false);
// Auth debugger state
const [authState, setAuthState] = useState<AuthDebuggerState>({
isInitiatingAuth: false,
oauthTokens: null,
loading: true,
oauthStep: "metadata_discovery",
oauthMetadata: null,
oauthClientInfo: null,
authorizationUrl: null,
authorizationCode: "",
latestError: null,
statusMessage: null,
validationError: null,
});
// Helper function to update specific auth state properties
const updateAuthState = (updates: Partial<AuthDebuggerState>) => {
setAuthState((prev) => ({ ...prev, ...updates }));
};
const nextRequestId = useRef(0);
const rootsRef = useRef<Root[]>([]);
@@ -208,11 +235,64 @@ const App = () => {
(serverUrl: string) => {
setSseUrl(serverUrl);
setTransportType("sse");
setIsAuthDebuggerVisible(false);
void connectMcpServer();
},
[connectMcpServer],
);
// Update OAuth debug state during debug callback
const onOAuthDebugConnect = useCallback(
({
authorizationCode,
errorMsg,
}: {
authorizationCode?: string;
errorMsg?: string;
}) => {
setIsAuthDebuggerVisible(true);
if (authorizationCode) {
updateAuthState({
authorizationCode,
oauthStep: "token_request",
});
}
if (errorMsg) {
updateAuthState({
latestError: new Error(errorMsg),
});
}
},
[],
);
// Load OAuth tokens when sseUrl changes
useEffect(() => {
const loadOAuthTokens = async () => {
try {
if (sseUrl) {
const key = getServerSpecificKey(SESSION_KEYS.TOKENS, sseUrl);
const tokens = sessionStorage.getItem(key);
if (tokens) {
const parsedTokens = await OAuthTokensSchema.parseAsync(
JSON.parse(tokens),
);
updateAuthState({
oauthTokens: parsedTokens,
oauthStep: "complete",
});
}
}
} catch (error) {
console.error("Error loading OAuth tokens:", error);
} finally {
updateAuthState({ loading: false });
}
};
loadOAuthTokens();
}, [sseUrl]);
useEffect(() => {
fetch(`${getMCPProxyAddress(config)}/config`)
.then((response) => response.json())
@@ -446,6 +526,19 @@ const App = () => {
setStdErrNotifications([]);
};
// Helper component for rendering the AuthDebugger
const AuthDebuggerWrapper = () => (
<TabsContent value="auth">
<AuthDebugger
serverUrl={sseUrl}
onBack={() => setIsAuthDebuggerVisible(false)}
authState={authState}
updateAuthState={updateAuthState}
/>
</TabsContent>
);
// Helper function to render OAuth callback components
if (window.location.pathname === "/oauth/callback") {
const OAuthCallback = React.lazy(
() => import("./components/OAuthCallback"),
@@ -457,6 +550,17 @@ const App = () => {
);
}
if (window.location.pathname === "/oauth/callback/debug") {
const OAuthDebugCallback = React.lazy(
() => import("./components/OAuthDebugCallback"),
);
return (
<Suspense fallback={<div>Loading...</div>}>
<OAuthDebugCallback onConnect={onOAuthDebugConnect} />
</Suspense>
);
}
return (
<div className="flex h-screen bg-background">
<Sidebar
@@ -544,6 +648,10 @@ const App = () => {
<FolderTree className="w-4 h-4 mr-2" />
Roots
</TabsTrigger>
<TabsTrigger value="auth">
<Key className="w-4 h-4 mr-2" />
Auth
</TabsTrigger>
</TabsList>
<div className="w-full">
@@ -689,15 +797,36 @@ const App = () => {
setRoots={setRoots}
onRootsChange={handleRootsChange}
/>
<AuthDebuggerWrapper />
</>
)}
</div>
</Tabs>
) : isAuthDebuggerVisible ? (
<Tabs
defaultValue={"auth"}
className="w-full p-4"
onValueChange={(value) => (window.location.hash = value)}
>
<AuthDebuggerWrapper />
</Tabs>
) : (
<div className="flex items-center justify-center h-full">
<div className="flex flex-col items-center justify-center h-full gap-4">
<p className="text-lg text-gray-500">
Connect to an MCP server to start inspecting
</p>
<div className="flex items-center gap-2">
<p className="text-sm text-muted-foreground">
Need to configure authentication?
</p>
<Button
variant="outline"
size="sm"
onClick={() => setIsAuthDebuggerVisible(true)}
>
Open Auth Settings
</Button>
</div>
</div>
)}
</div>