diff --git a/client/src/App.tsx b/client/src/App.tsx index 01dc987..1d1556e 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -10,11 +10,13 @@ import { ListPromptsResultSchema, ListResourcesResultSchema, ListResourceTemplatesResultSchema, + ListRootsRequestSchema, ListToolsResultSchema, ProgressNotificationSchema, ReadResourceResultSchema, Resource, ResourceTemplate, + Root, ServerNotification, Tool, } from "@modelcontextprotocol/sdk/types.js"; @@ -39,6 +41,7 @@ import { Play, Send, Terminal, + FolderTree, } from "lucide-react"; import { AnyZodObject } from "zod"; @@ -49,6 +52,7 @@ import PingTab from "./components/PingTab"; import PromptsTab, { Prompt } from "./components/PromptsTab"; import RequestsTab from "./components/RequestsTabs"; import ResourcesTab from "./components/ResourcesTab"; +import RootsTab from "./components/RootsTab"; import SamplingTab, { PendingRequest } from "./components/SamplingTab"; import Sidebar from "./components/Sidebar"; import ToolsTab from "./components/ToolsTab"; @@ -86,6 +90,7 @@ const App = () => { >([]); const [mcpClient, setMcpClient] = useState(null); const [notifications, setNotifications] = useState([]); + const [roots, setRoots] = useState([]); const [pendingSampleRequests, setPendingSampleRequests] = useState< Array< @@ -254,6 +259,16 @@ const App = () => { setToolResult(JSON.stringify(response.toolResult, null, 2)); }; + const handleRootsChange = async () => { + if (mcpClient) { + try { + await mcpClient.sendRootsListChanged(); + } catch (e) { + console.error("Failed to send roots list changed notification:", e); + } + } + }; + const connectMcpServer = async () => { try { const client = new Client({ @@ -293,6 +308,10 @@ const App = () => { }); }); + client.setRequestHandler(ListRootsRequestSchema, async () => { + return { roots }; + }); + setMcpClient(client); setConnectionStatus("connected"); } catch (e) { @@ -387,6 +406,10 @@ const App = () => { )} + + + Roots +
@@ -443,6 +466,11 @@ const App = () => { onApprove={handleApproveSampling} onReject={handleRejectSampling} /> +
) : ( diff --git a/client/src/components/RootsTab.tsx b/client/src/components/RootsTab.tsx new file mode 100644 index 0000000..2c6f5fb --- /dev/null +++ b/client/src/components/RootsTab.tsx @@ -0,0 +1,84 @@ +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { TabsContent } from "@/components/ui/tabs"; +import { Root } from "@modelcontextprotocol/sdk/types.js"; +import { Plus, Minus, Save } from "lucide-react"; +import { useCallback } from "react"; + +const RootsTab = ({ + roots, + setRoots, + onRootsChange, +}: { + roots: Root[]; + setRoots: React.Dispatch>; + onRootsChange: () => void; +}) => { + const addRoot = useCallback(() => { + setRoots((currentRoots) => [...currentRoots, { uri: "file://", name: "" }]); + }, [setRoots]); + + const removeRoot = useCallback( + (index: number) => { + setRoots((currentRoots) => currentRoots.filter((_, i) => i !== index)); + }, + [setRoots], + ); + + const updateRoot = useCallback( + (index: number, field: keyof Root, value: string) => { + setRoots((currentRoots) => + currentRoots.map((root, i) => + i === index ? { ...root, [field]: value } : root, + ), + ); + }, + [setRoots], + ); + + const handleSave = useCallback(() => { + onRootsChange(); + }, [onRootsChange]); + + return ( + + + + Configure the root directories that the server can access + + + + {roots.map((root, index) => ( +
+ updateRoot(index, "uri", e.target.value)} + className="flex-1" + /> + +
+ ))} + +
+ + +
+
+ ); +}; + +export default RootsTab;