This fixes the linter's warning that Hooks must be run in order each time.

* In App.tsx
  - move the conditional that returns suspense if the path is oauth/callback to the end of the component after all hooks, rendering either suspense or the normal component.
  - move handleApproveSampling and handleRejectSampling functions down below all the hooks for clarity. There are a lot of hooks so finding the end of them is a scroll, and these function constants aren't referenced until the rendering section below anyway.
This commit is contained in:
cliffhall
2025-03-27 15:59:41 -04:00
parent 4d4bb9110c
commit 0891077402

View File

@@ -52,16 +52,6 @@ const PROXY_SERVER_URL = `http://${window.location.hostname}:${PROXY_PORT}`;
const App = () => { const App = () => {
// Handle OAuth callback route // Handle OAuth callback route
if (window.location.pathname === "/oauth/callback") {
const OAuthCallback = React.lazy(
() => import("./components/OAuthCallback"),
);
return (
<Suspense fallback={<div>Loading...</div>}>
<OAuthCallback />
</Suspense>
);
}
const [resources, setResources] = useState<Resource[]>([]); const [resources, setResources] = useState<Resource[]>([]);
const [resourceTemplates, setResourceTemplates] = useState< const [resourceTemplates, setResourceTemplates] = useState<
ResourceTemplate[] ResourceTemplate[]
@@ -114,22 +104,6 @@ const App = () => {
const nextRequestId = useRef(0); const nextRequestId = useRef(0);
const rootsRef = useRef<Root[]>([]); const rootsRef = useRef<Root[]>([]);
const handleApproveSampling = (id: number, result: CreateMessageResult) => {
setPendingSampleRequests((prev) => {
const request = prev.find((r) => r.id === id);
request?.resolve(result);
return prev.filter((r) => r.id !== id);
});
};
const handleRejectSampling = (id: number) => {
setPendingSampleRequests((prev) => {
const request = prev.find((r) => r.id === id);
request?.reject(new Error("Sampling request rejected"));
return prev.filter((r) => r.id !== id);
});
};
const [selectedResource, setSelectedResource] = useState<Resource | null>( const [selectedResource, setSelectedResource] = useState<Resource | null>(
null, null,
); );
@@ -224,7 +198,7 @@ const App = () => {
// Connect to the server // Connect to the server
connectMcpServer(); connectMcpServer();
} }
}, []); }, [connectMcpServer]);
useEffect(() => { useEffect(() => {
fetch(`${PROXY_SERVER_URL}/config`) fetch(`${PROXY_SERVER_URL}/config`)
@@ -253,6 +227,22 @@ const App = () => {
} }
}, []); }, []);
const handleApproveSampling = (id: number, result: CreateMessageResult) => {
setPendingSampleRequests((prev) => {
const request = prev.find((r) => r.id === id);
request?.resolve(result);
return prev.filter((r) => r.id !== id);
});
};
const handleRejectSampling = (id: number) => {
setPendingSampleRequests((prev) => {
const request = prev.find((r) => r.id === id);
request?.reject(new Error("Sampling request rejected"));
return prev.filter((r) => r.id !== id);
});
};
const clearError = (tabKey: keyof typeof errors) => { const clearError = (tabKey: keyof typeof errors) => {
setErrors((prev) => ({ ...prev, [tabKey]: null })); setErrors((prev) => ({ ...prev, [tabKey]: null }));
}; };
@@ -425,251 +415,263 @@ const App = () => {
setLogLevel(level); setLogLevel(level);
}; };
return ( if (window.location.pathname === "/oauth/callback") {
<div className="flex h-screen bg-background"> const OAuthCallback = React.lazy(
<Sidebar () => import("./components/OAuthCallback"),
connectionStatus={connectionStatus} );
transportType={transportType} return (
setTransportType={setTransportType} <Suspense fallback={<div>Loading...</div>}>
command={command} <OAuthCallback />
setCommand={setCommand} </Suspense>
args={args} );
setArgs={setArgs} } else {
sseUrl={sseUrl} return (
setSseUrl={setSseUrl} <div className="flex h-screen bg-background">
env={env} <Sidebar
setEnv={setEnv} connectionStatus={connectionStatus}
bearerToken={bearerToken} transportType={transportType}
setBearerToken={setBearerToken} setTransportType={setTransportType}
onConnect={connectMcpServer} command={command}
stdErrNotifications={stdErrNotifications} setCommand={setCommand}
logLevel={logLevel} args={args}
sendLogLevelRequest={sendLogLevelRequest} setArgs={setArgs}
loggingSupported={!!serverCapabilities?.logging || false} sseUrl={sseUrl}
/> setSseUrl={setSseUrl}
<div className="flex-1 flex flex-col overflow-hidden"> env={env}
<div className="flex-1 overflow-auto"> setEnv={setEnv}
{mcpClient ? ( bearerToken={bearerToken}
<Tabs setBearerToken={setBearerToken}
defaultValue={ onConnect={connectMcpServer}
Object.keys(serverCapabilities ?? {}).includes( stdErrNotifications={stdErrNotifications}
window.location.hash.slice(1), logLevel={logLevel}
) sendLogLevelRequest={sendLogLevelRequest}
? window.location.hash.slice(1) loggingSupported={!!serverCapabilities?.logging || false}
: serverCapabilities?.resources />
? "resources" <div className="flex-1 flex flex-col overflow-hidden">
: serverCapabilities?.prompts <div className="flex-1 overflow-auto">
? "prompts" {mcpClient ? (
: serverCapabilities?.tools <Tabs
? "tools" defaultValue={
: "ping" Object.keys(serverCapabilities ?? {}).includes(
} window.location.hash.slice(1),
className="w-full p-4" )
onValueChange={(value) => (window.location.hash = value)} ? window.location.hash.slice(1)
> : serverCapabilities?.resources
<TabsList className="mb-4 p-0"> ? "resources"
<TabsTrigger : serverCapabilities?.prompts
value="resources" ? "prompts"
disabled={!serverCapabilities?.resources} : serverCapabilities?.tools
> ? "tools"
<Files className="w-4 h-4 mr-2" /> : "ping"
Resources }
</TabsTrigger> className="w-full p-4"
<TabsTrigger onValueChange={(value) => (window.location.hash = value)}
value="prompts" >
disabled={!serverCapabilities?.prompts} <TabsList className="mb-4 p-0">
> <TabsTrigger
<MessageSquare className="w-4 h-4 mr-2" /> value="resources"
Prompts disabled={!serverCapabilities?.resources}
</TabsTrigger> >
<TabsTrigger <Files className="w-4 h-4 mr-2" />
value="tools" Resources
disabled={!serverCapabilities?.tools} </TabsTrigger>
> <TabsTrigger
<Hammer className="w-4 h-4 mr-2" /> value="prompts"
Tools disabled={!serverCapabilities?.prompts}
</TabsTrigger> >
<TabsTrigger value="ping"> <MessageSquare className="w-4 h-4 mr-2" />
<Bell className="w-4 h-4 mr-2" /> Prompts
Ping </TabsTrigger>
</TabsTrigger> <TabsTrigger
<TabsTrigger value="sampling" className="relative"> value="tools"
<Hash className="w-4 h-4 mr-2" /> disabled={!serverCapabilities?.tools}
Sampling >
{pendingSampleRequests.length > 0 && ( <Hammer className="w-4 h-4 mr-2" />
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center"> Tools
{pendingSampleRequests.length} </TabsTrigger>
</span> <TabsTrigger value="ping">
)} <Bell className="w-4 h-4 mr-2" />
</TabsTrigger> Ping
<TabsTrigger value="roots"> </TabsTrigger>
<FolderTree className="w-4 h-4 mr-2" /> <TabsTrigger value="sampling" className="relative">
Roots <Hash className="w-4 h-4 mr-2" />
</TabsTrigger> Sampling
</TabsList> {pendingSampleRequests.length > 0 && (
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
{pendingSampleRequests.length}
</span>
)}
</TabsTrigger>
<TabsTrigger value="roots">
<FolderTree className="w-4 h-4 mr-2" />
Roots
</TabsTrigger>
</TabsList>
<div className="w-full"> <div className="w-full">
{!serverCapabilities?.resources && {!serverCapabilities?.resources &&
!serverCapabilities?.prompts && !serverCapabilities?.prompts &&
!serverCapabilities?.tools ? ( !serverCapabilities?.tools ? (
<div className="flex items-center justify-center p-4"> <div className="flex items-center justify-center p-4">
<p className="text-lg text-gray-500"> <p className="text-lg text-gray-500">
The connected server does not support any MCP capabilities The connected server does not support any MCP
</p> capabilities
</div> </p>
) : ( </div>
<> ) : (
<ResourcesTab <>
resources={resources} <ResourcesTab
resourceTemplates={resourceTemplates} resources={resources}
listResources={() => { resourceTemplates={resourceTemplates}
clearError("resources"); listResources={() => {
listResources(); clearError("resources");
}} listResources();
clearResources={() => { }}
setResources([]); clearResources={() => {
setNextResourceCursor(undefined); setResources([]);
}} setNextResourceCursor(undefined);
listResourceTemplates={() => { }}
clearError("resources"); listResourceTemplates={() => {
listResourceTemplates(); clearError("resources");
}} listResourceTemplates();
clearResourceTemplates={() => { }}
setResourceTemplates([]); clearResourceTemplates={() => {
setNextResourceTemplateCursor(undefined); setResourceTemplates([]);
}} setNextResourceTemplateCursor(undefined);
readResource={(uri) => { }}
clearError("resources"); readResource={(uri) => {
readResource(uri); clearError("resources");
}} readResource(uri);
selectedResource={selectedResource} }}
setSelectedResource={(resource) => { selectedResource={selectedResource}
clearError("resources"); setSelectedResource={(resource) => {
setSelectedResource(resource); clearError("resources");
}} setSelectedResource(resource);
resourceSubscriptionsSupported={ }}
serverCapabilities?.resources?.subscribe || false resourceSubscriptionsSupported={
} serverCapabilities?.resources?.subscribe || false
resourceSubscriptions={resourceSubscriptions} }
subscribeToResource={(uri) => { resourceSubscriptions={resourceSubscriptions}
clearError("resources"); subscribeToResource={(uri) => {
subscribeToResource(uri); clearError("resources");
}} subscribeToResource(uri);
unsubscribeFromResource={(uri) => { }}
clearError("resources"); unsubscribeFromResource={(uri) => {
unsubscribeFromResource(uri); clearError("resources");
}} unsubscribeFromResource(uri);
handleCompletion={handleCompletion} }}
completionsSupported={completionsSupported} handleCompletion={handleCompletion}
resourceContent={resourceContent} completionsSupported={completionsSupported}
nextCursor={nextResourceCursor} resourceContent={resourceContent}
nextTemplateCursor={nextResourceTemplateCursor} nextCursor={nextResourceCursor}
error={errors.resources} nextTemplateCursor={nextResourceTemplateCursor}
/> error={errors.resources}
<PromptsTab />
prompts={prompts} <PromptsTab
listPrompts={() => { prompts={prompts}
clearError("prompts"); listPrompts={() => {
listPrompts(); clearError("prompts");
}} listPrompts();
clearPrompts={() => { }}
setPrompts([]); clearPrompts={() => {
setNextPromptCursor(undefined); setPrompts([]);
}} setNextPromptCursor(undefined);
getPrompt={(name, args) => { }}
clearError("prompts"); getPrompt={(name, args) => {
getPrompt(name, args); clearError("prompts");
}} getPrompt(name, args);
selectedPrompt={selectedPrompt} }}
setSelectedPrompt={(prompt) => { selectedPrompt={selectedPrompt}
clearError("prompts"); setSelectedPrompt={(prompt) => {
setSelectedPrompt(prompt); clearError("prompts");
}} setSelectedPrompt(prompt);
handleCompletion={handleCompletion} }}
completionsSupported={completionsSupported} handleCompletion={handleCompletion}
promptContent={promptContent} completionsSupported={completionsSupported}
nextCursor={nextPromptCursor} promptContent={promptContent}
error={errors.prompts} nextCursor={nextPromptCursor}
/> error={errors.prompts}
<ToolsTab />
tools={tools} <ToolsTab
listTools={() => { tools={tools}
clearError("tools"); listTools={() => {
listTools(); clearError("tools");
}} listTools();
clearTools={() => { }}
setTools([]); clearTools={() => {
setNextToolCursor(undefined); setTools([]);
}} setNextToolCursor(undefined);
callTool={(name, params) => { }}
clearError("tools"); callTool={(name, params) => {
callTool(name, params); clearError("tools");
}} callTool(name, params);
selectedTool={selectedTool} }}
setSelectedTool={(tool) => { selectedTool={selectedTool}
clearError("tools"); setSelectedTool={(tool) => {
setSelectedTool(tool); clearError("tools");
setToolResult(null); setSelectedTool(tool);
}} setToolResult(null);
toolResult={toolResult} }}
nextCursor={nextToolCursor} toolResult={toolResult}
error={errors.tools} nextCursor={nextToolCursor}
/> error={errors.tools}
<ConsoleTab /> />
<PingTab <ConsoleTab />
onPingClick={() => { <PingTab
void makeRequest( onPingClick={() => {
{ void makeRequest(
method: "ping" as const, {
}, method: "ping" as const,
EmptyResultSchema, },
); EmptyResultSchema,
}} );
/> }}
<SamplingTab />
pendingRequests={pendingSampleRequests} <SamplingTab
onApprove={handleApproveSampling} pendingRequests={pendingSampleRequests}
onReject={handleRejectSampling} onApprove={handleApproveSampling}
/> onReject={handleRejectSampling}
<RootsTab />
roots={roots} <RootsTab
setRoots={setRoots} roots={roots}
onRootsChange={handleRootsChange} setRoots={setRoots}
/> onRootsChange={handleRootsChange}
</> />
)} </>
)}
</div>
</Tabs>
) : (
<div className="flex items-center justify-center h-full">
<p className="text-lg text-gray-500">
Connect to an MCP server to start inspecting
</p>
</div> </div>
</Tabs> )}
) : (
<div className="flex items-center justify-center h-full">
<p className="text-lg text-gray-500">
Connect to an MCP server to start inspecting
</p>
</div>
)}
</div>
<div
className="relative border-t border-border"
style={{
height: `${historyPaneHeight}px`,
}}
>
<div
className="absolute w-full h-4 -top-2 cursor-row-resize flex items-center justify-center hover:bg-accent/50"
onMouseDown={handleDragStart}
>
<div className="w-8 h-1 rounded-full bg-border" />
</div> </div>
<div className="h-full overflow-auto"> <div
<HistoryAndNotifications className="relative border-t border-border"
requestHistory={requestHistory} style={{
serverNotifications={notifications} height: `${historyPaneHeight}px`,
/> }}
>
<div
className="absolute w-full h-4 -top-2 cursor-row-resize flex items-center justify-center hover:bg-accent/50"
onMouseDown={handleDragStart}
>
<div className="w-8 h-1 rounded-full bg-border" />
</div>
<div className="h-full overflow-auto">
<HistoryAndNotifications
requestHistory={requestHistory}
serverNotifications={notifications}
/>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> );
); }
}; };
export default App; export default App;