Merge pull request #54 from modelcontextprotocol/justin/redo-sidebars
Revamp UI + panes
This commit is contained in:
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user