Merge pull request #54 from modelcontextprotocol/justin/redo-sidebars

Revamp UI + panes
This commit is contained in:
Justin Spahr-Summers
2024-11-12 15:31:21 +00:00
committed by GitHub
4 changed files with 375 additions and 266 deletions

View File

@@ -22,21 +22,12 @@ import {
CompatibilityCallToolResult, CompatibilityCallToolResult,
ClientNotification, ClientNotification,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
// Add dark mode class based on system preference // Add dark mode class based on system preference
if (window.matchMedia("(prefers-color-scheme: dark)").matches) { if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
document.documentElement.classList.add("dark"); document.documentElement.classList.add("dark");
} }
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { import {
Bell, Bell,
@@ -44,12 +35,9 @@ import {
Hammer, Hammer,
Hash, Hash,
MessageSquare, MessageSquare,
Play,
Send, Send,
Terminal, Terminal,
FolderTree, FolderTree,
ChevronDown,
ChevronRight,
} from "lucide-react"; } from "lucide-react";
import { ZodType } from "zod"; import { ZodType } from "zod";
@@ -95,7 +83,6 @@ const App = () => {
const [notifications, setNotifications] = useState<ServerNotification[]>([]); const [notifications, setNotifications] = useState<ServerNotification[]>([]);
const [roots, setRoots] = useState<Root[]>([]); const [roots, setRoots] = useState<Root[]>([]);
const [env, setEnv] = useState<Record<string, string>>({}); const [env, setEnv] = useState<Record<string, string>>({});
const [showEnvVars, setShowEnvVars] = useState(false);
const [pendingSampleRequests, setPendingSampleRequests] = useState< const [pendingSampleRequests, setPendingSampleRequests] = useState<
Array< Array<
@@ -140,6 +127,49 @@ const App = () => {
>(); >();
const [nextToolCursor, setNextToolCursor] = useState<string | undefined>(); const [nextToolCursor, setNextToolCursor] = useState<string | undefined>();
const progressTokenRef = useRef(0); const progressTokenRef = useRef(0);
const [historyPaneHeight, setHistoryPaneHeight] = useState<number>(300);
const [isDragging, setIsDragging] = useState(false);
const dragStartY = useRef<number>(0);
const dragStartHeight = useRef<number>(0);
const handleDragStart = useCallback(
(e: React.MouseEvent) => {
setIsDragging(true);
dragStartY.current = e.clientY;
dragStartHeight.current = historyPaneHeight;
document.body.style.userSelect = "none";
},
[historyPaneHeight],
);
const handleDragMove = useCallback(
(e: MouseEvent) => {
if (!isDragging) return;
const deltaY = dragStartY.current - e.clientY;
const newHeight = Math.max(
100,
Math.min(800, dragStartHeight.current + deltaY),
);
setHistoryPaneHeight(newHeight);
},
[isDragging],
);
const handleDragEnd = useCallback(() => {
setIsDragging(false);
document.body.style.userSelect = "";
}, []);
useEffect(() => {
if (isDragging) {
window.addEventListener("mousemove", handleDragMove);
window.addEventListener("mouseup", handleDragEnd);
return () => {
window.removeEventListener("mousemove", handleDragMove);
window.removeEventListener("mouseup", handleDragEnd);
};
}
}, [isDragging, handleDragMove, handleDragEnd]);
useEffect(() => { useEffect(() => {
localStorage.setItem("lastCommand", command); localStorage.setItem("lastCommand", command);
@@ -353,114 +383,22 @@ const App = () => {
return ( return (
<div className="flex h-screen bg-background"> <div className="flex h-screen bg-background">
<Sidebar connectionStatus={connectionStatus} /> <Sidebar
connectionStatus={connectionStatus}
transportType={transportType}
setTransportType={setTransportType}
command={command}
setCommand={setCommand}
args={args}
setArgs={setArgs}
url={url}
setUrl={setUrl}
env={env}
setEnv={setEnv}
onConnect={connectMcpServer}
/>
<div className="flex-1 flex flex-col overflow-hidden"> <div className="flex-1 flex flex-col overflow-hidden">
<h1 className="text-2xl font-bold p-4">MCP Inspector</h1> <div className="flex-1 overflow-auto">
<div className="flex-1 overflow-auto flex">
<div className="flex-1">
<div className="p-4 bg-card shadow-md m-4 rounded-md">
<h2 className="text-lg font-semibold mb-2">Connect MCP Server</h2>
<div className="flex space-x-2 mb-2">
<Select
value={transportType}
onValueChange={(value: "stdio" | "sse") =>
setTransportType(value)
}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Select transport type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="stdio">STDIO</SelectItem>
<SelectItem value="sse">SSE</SelectItem>
</SelectContent>
</Select>
{transportType === "stdio" ? (
<>
<Input
placeholder="Command"
value={command}
onChange={(e) => setCommand(e.target.value)}
/>
<Input
placeholder="Arguments (space-separated)"
value={args}
onChange={(e) => setArgs(e.target.value)}
/>
</>
) : (
<Input
placeholder="URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
)}
<Button onClick={connectMcpServer}>
<Play className="w-4 h-4 mr-2" />
Connect
</Button>
</div>
{transportType === "stdio" && (
<div className="mt-4">
<Button
variant="outline"
onClick={() => setShowEnvVars(!showEnvVars)}
className="flex items-center"
>
{showEnvVars ? (
<ChevronDown className="w-4 h-4 mr-2" />
) : (
<ChevronRight className="w-4 h-4 mr-2" />
)}
Environment Variables
</Button>
{showEnvVars && (
<div className="mt-2">
{Object.entries(env).map(([key, value]) => (
<div key={key} className="flex space-x-2 mb-2">
<Input
placeholder="Key"
value={key}
onChange={(e) =>
setEnv((prev) => ({
...prev,
[e.target.value]: value,
}))
}
/>
<Input
placeholder="Value"
value={value}
onChange={(e) =>
setEnv((prev) => ({
...prev,
[key]: e.target.value,
}))
}
/>
<Button
onClick={() =>
setEnv((prev) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [key]: _, ...rest } = prev;
return rest;
})
}
>
Remove
</Button>
</div>
))}
<Button
onClick={() => setEnv((prev) => ({ ...prev, "": "" }))}
>
Add Environment Variable
</Button>
</div>
)}
</div>
)}
</div>
{mcpClient ? ( {mcpClient ? (
<Tabs defaultValue="resources" className="w-full p-4"> <Tabs defaultValue="resources" className="w-full p-4">
<TabsList className="mb-4 p-0"> <TabsList className="mb-4 p-0">
@@ -572,13 +510,27 @@ const App = () => {
</div> </div>
)} )}
</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> <div className="h-full overflow-auto">
<HistoryAndNotifications <HistoryAndNotifications
requestHistory={requestHistory} requestHistory={requestHistory}
serverNotifications={notifications} serverNotifications={notifications}
/> />
</div> </div>
</div>
</div>
</div>
); );
}; };

View File

@@ -29,8 +29,8 @@ const HistoryAndNotifications = ({
}; };
return ( return (
<div className="w-64 bg-card shadow-md p-4 overflow-hidden flex flex-col h-full"> <div className="bg-card overflow-hidden flex h-full">
<div className="flex-1 overflow-y-auto mb-4 border-b pb-4"> <div className="flex-1 overflow-y-auto p-4 border-r">
<h2 className="text-lg font-semibold mb-4">History</h2> <h2 className="text-lg font-semibold mb-4">History</h2>
{requestHistory.length === 0 ? ( {requestHistory.length === 0 ? (
<p className="text-sm text-gray-500 italic">No history yet</p> <p className="text-sm text-gray-500 italic">No history yet</p>
@@ -107,7 +107,7 @@ const HistoryAndNotifications = ({
</ul> </ul>
)} )}
</div> </div>
<div className="flex-1 overflow-y-auto"> <div className="flex-1 overflow-y-auto p-4">
<h2 className="text-lg font-semibold mb-4">Server Notifications</h2> <h2 className="text-lg font-semibold mb-4">Server Notifications</h2>
{serverNotifications.length === 0 ? ( {serverNotifications.length === 0 ? (
<p className="text-sm text-gray-500 italic">No notifications yet</p> <p className="text-sm text-gray-500 italic">No notifications yet</p>

View File

@@ -1,15 +1,174 @@
import { Menu, Settings } from "lucide-react"; import { useState } from "react";
import { Button } from "@/components/ui/button";
const Sidebar = ({ connectionStatus }: { connectionStatus: string }) => ( import { Play, ChevronDown, ChevronRight, Settings } from "lucide-react";
<div className="w-64 bg-card border-r border-border"> import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
interface SidebarProps {
connectionStatus: "disconnected" | "connected" | "error";
transportType: "stdio" | "sse";
setTransportType: (type: "stdio" | "sse") => void;
command: string;
setCommand: (command: string) => void;
args: string;
setArgs: (args: string) => void;
url: string;
setUrl: (url: string) => void;
env: Record<string, string>;
setEnv: (env: Record<string, string>) => void;
onConnect: () => void;
}
const Sidebar = ({
connectionStatus,
transportType,
setTransportType,
command,
setCommand,
args,
setArgs,
url,
setUrl,
env,
setEnv,
onConnect,
}: SidebarProps) => {
const [showEnvVars, setShowEnvVars] = useState(false);
return (
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
<div className="flex items-center p-4 border-b border-gray-200"> <div className="flex items-center p-4 border-b border-gray-200">
<Menu className="w-6 h-6 text-gray-500" /> <Settings className="w-6 h-6 text-gray-500" />
<h1 className="ml-2 text-lg font-semibold">MCP Inspector</h1> <h1 className="ml-2 text-lg font-semibold">MCP Inspector</h1>
</div> </div>
<div className="p-4"> <div className="p-4 flex-1 overflow-auto">
<div className="flex items-center space-x-2 mb-4"> <div className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Transport Type</label>
<Select
value={transportType}
onValueChange={(value: "stdio" | "sse") =>
setTransportType(value)
}
>
<SelectTrigger>
<SelectValue placeholder="Select transport type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="stdio">STDIO</SelectItem>
<SelectItem value="sse">SSE</SelectItem>
</SelectContent>
</Select>
</div>
{transportType === "stdio" ? (
<>
<div className="space-y-2">
<label className="text-sm font-medium">Command</label>
<Input
placeholder="Command"
value={command}
onChange={(e) => setCommand(e.target.value)}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Arguments</label>
<Input
placeholder="Arguments (space-separated)"
value={args}
onChange={(e) => setArgs(e.target.value)}
/>
</div>
</>
) : (
<div className="space-y-2">
<label className="text-sm font-medium">URL</label>
<Input
placeholder="URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</div>
)}
{transportType === "stdio" && (
<div className="space-y-2">
<Button
variant="outline"
onClick={() => setShowEnvVars(!showEnvVars)}
className="flex items-center w-full"
>
{showEnvVars ? (
<ChevronDown className="w-4 h-4 mr-2" />
) : (
<ChevronRight className="w-4 h-4 mr-2" />
)}
Environment Variables
</Button>
{showEnvVars && (
<div className="space-y-2">
{Object.entries(env).map(([key, value]) => (
<div key={key} className="grid grid-cols-[1fr,auto] gap-2">
<div className="space-y-1">
<Input
placeholder="Key"
value={key}
onChange={(e) => {
const newEnv = { ...env };
newEnv[e.target.value] = value;
setEnv(newEnv);
}}
/>
<Input
placeholder="Value"
value={value}
onChange={(e) => {
const newEnv = { ...env };
newEnv[key] = e.target.value;
setEnv(newEnv);
}}
/>
</div>
<Button
variant="destructive"
onClick={() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [key]: removed, ...rest } = env;
setEnv(rest);
}}
>
Remove
</Button>
</div>
))}
<Button
variant="outline"
onClick={() => {
const newEnv = { ...env };
newEnv[""] = "";
setEnv(newEnv);
}}
>
Add Environment Variable
</Button>
</div>
)}
</div>
)}
<div className="space-y-2">
<Button className="w-full" onClick={onConnect}>
<Play className="w-4 h-4 mr-2" />
Connect
</Button>
<div className="flex items-center justify-center space-x-2 mb-4">
<div <div
className={`w-2 h-2 rounded-full ${ className={`w-2 h-2 rounded-full ${
connectionStatus === "connected" connectionStatus === "connected"
@@ -27,13 +186,11 @@ const Sidebar = ({ connectionStatus }: { connectionStatus: string }) => (
: "Disconnected"} : "Disconnected"}
</span> </span>
</div> </div>
<Button variant="outline" className="w-full justify-start">
<Settings className="w-4 h-4 mr-2" />
Connection Settings
</Button>
</div> </div>
</div> </div>
); </div>
</div>
);
};
export default Sidebar; export default Sidebar;