From d80214d0a2c746a87289492b75aa52b238baaf82 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Thu, 7 Nov 2024 13:46:21 +0000 Subject: [PATCH 1/9] Update SDK to 0.3.2 --- package-lock.json | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index cd16d57a9..186417070 100644 --- a/package-lock.json +++ b/package-lock.json @@ -841,9 +841,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "0.1.0", - "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-internal/@modelcontextprotocol/sdk/-/@modelcontextprotocol/sdk-0.1.0.tgz", - "integrity": "sha512-46/FTHNZWUWbdFspKsCIixhCKwi9Hub5+HWNXC1DRIL2TSV8cdx5sTsg+Jy6I4Uc8/rd7tLZcVQ6IasIY1g4zg==", + "version": "0.3.2", + "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-internal/@modelcontextprotocol/sdk/-/@modelcontextprotocol/sdk-0.3.2.tgz", + "integrity": "sha512-7f4VYf43cH0VzewG1kdgoKZN56PA69VA2iix+/nlc0AOEJ6ClhebOpmeugK2ZvPctbzFuESaYW8Oh29u8Y09Jw==", "dependencies": { "content-type": "^1.0.5", "raw-body": "^3.0.0", @@ -4635,12 +4635,6 @@ } } }, - "node_modules/react-remove-scroll/node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "license": "0BSD" - }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -4811,13 +4805,6 @@ "tslib": "^2.1.0" } }, - "node_modules/rxjs/node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "dev": true, - "license": "0BSD" - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", From 645f2e942e0da41c59605e7e093352c69340cd45 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Thu, 7 Nov 2024 14:02:53 +0000 Subject: [PATCH 2/9] Add support for listing and filling resource templates --- client/src/App.tsx | 27 +++ client/src/components/ResourcesTab.tsx | 224 ++++++++++++++++++------- 2 files changed, 193 insertions(+), 58 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 055552a30..01dc98717 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -9,10 +9,12 @@ import { GetPromptResultSchema, ListPromptsResultSchema, ListResourcesResultSchema, + ListResourceTemplatesResultSchema, ListToolsResultSchema, ProgressNotificationSchema, ReadResourceResultSchema, Resource, + ResourceTemplate, ServerNotification, Tool, } from "@modelcontextprotocol/sdk/types.js"; @@ -56,6 +58,9 @@ const App = () => { "disconnected" | "connected" | "error" >("disconnected"); const [resources, setResources] = useState([]); + const [resourceTemplates, setResourceTemplates] = useState< + ResourceTemplate[] + >([]); const [resourceContent, setResourceContent] = useState(""); const [prompts, setPrompts] = useState([]); const [promptContent, setPromptContent] = useState(""); @@ -116,6 +121,9 @@ const App = () => { const [nextResourceCursor, setNextResourceCursor] = useState< string | undefined >(); + const [nextResourceTemplateCursor, setNextResourceTemplateCursor] = useState< + string | undefined + >(); const [nextPromptCursor, setNextPromptCursor] = useState< string | undefined >(); @@ -167,6 +175,22 @@ const App = () => { setNextResourceCursor(response.nextCursor); }; + const listResourceTemplates = async () => { + const response = await makeRequest( + { + method: "resources/templates/list" as const, + params: nextResourceTemplateCursor + ? { cursor: nextResourceTemplateCursor } + : {}, + }, + ListResourceTemplatesResultSchema, + ); + setResourceTemplates( + resourceTemplates.concat(response.resourceTemplates ?? []), + ); + setNextResourceTemplateCursor(response.nextCursor); + }; + const readResource = async (uri: string) => { const response = await makeRequest( { @@ -368,12 +392,15 @@ const App = () => {
void; + listResourceTemplates: () => void; readResource: (uri: string) => void; selectedResource: Resource | null; - setSelectedResource: (resource: Resource) => void; + setSelectedResource: (resource: Resource | null) => void; resourceContent: string; nextCursor: ListResourcesResult["nextCursor"]; + nextTemplateCursor: ListResourceTemplatesResult["nextCursor"]; error: string | null; -}) => ( - - { - setSelectedResource(resource); - readResource(resource.uri); - }} - renderItem={(resource) => ( -
- - - {resource.name} - - -
- )} - title="Resources" - buttonText={nextCursor ? "List More Resources" : "List Resources"} - isButtonDisabled={!nextCursor && resources.length > 0} - /> +}) => { + const [selectedTemplate, setSelectedTemplate] = + useState(null); + const [templateValues, setTemplateValues] = useState>( + {}, + ); -
-
-

- {selectedResource ? selectedResource.name : "Select a resource"} -

- {selectedResource && ( - + const fillTemplate = ( + template: string, + values: Record, + ): string => { + return template.replace( + /{([^}]+)}/g, + (_, key) => values[key] || `{${key}}`, + ); + }; + + const handleReadTemplateResource = () => { + if (selectedTemplate) { + const uri = fillTemplate(selectedTemplate.uriTemplate, templateValues); + readResource(uri); + setSelectedTemplate(null); + // We don't have the full Resource object here, so we create a partial one + setSelectedResource({ uri, name: uri } as Resource); + } + }; + + return ( + + { + setSelectedResource(resource); + readResource(resource.uri); + setSelectedTemplate(null); + }} + renderItem={(resource) => ( +
+ + + {resource.name} + + +
)} -
-
- {error ? ( - - - Error - {error} - - ) : selectedResource ? ( -
-            {resourceContent}
-          
- ) : ( - - - Select a resource from the list to view its contents - - + title="Resources" + buttonText={nextCursor ? "List More Resources" : "List Resources"} + isButtonDisabled={!nextCursor && resources.length > 0} + /> + + { + setSelectedTemplate(template); + setSelectedResource(null); + setTemplateValues({}); + }} + renderItem={(template) => ( +
+ + + {template.name} + + +
)} + title="Resource Templates" + buttonText={ + nextTemplateCursor ? "List More Templates" : "List Templates" + } + isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0} + /> + +
+
+

+ {selectedResource + ? selectedResource.name + : selectedTemplate + ? selectedTemplate.name + : "Select a resource or template"} +

+ {selectedResource && ( + + )} +
+
+ {error ? ( + + + Error + {error} + + ) : selectedResource ? ( +
+              {resourceContent}
+            
+ ) : selectedTemplate ? ( +
+

+ {selectedTemplate.description} +

+ {selectedTemplate.uriTemplate + .match(/{([^}]+)}/g) + ?.map((param) => { + const key = param.slice(1, -1); + return ( +
+ + + setTemplateValues({ + ...templateValues, + [key]: e.target.value, + }) + } + className="mt-1" + /> +
+ ); + })} + +
+ ) : ( + + + Select a resource or template from the list to view its contents + + + )} +
-
- -); + + ); +}; export default ResourcesTab; From f3406ca43d1d6ff9515903f40fc4dade89015005 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Thu, 7 Nov 2024 14:11:48 +0000 Subject: [PATCH 3/9] Basic support for roots --- client/src/App.tsx | 28 ++++++++++ client/src/components/RootsTab.tsx | 84 ++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) create mode 100644 client/src/components/RootsTab.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 01dc98717..1d1556e61 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 000000000..2c6f5fb28 --- /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; From 193032533bafffb6a21554c062454527857a4bec Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Thu, 7 Nov 2024 15:17:18 +0000 Subject: [PATCH 4/9] Support structured tool results --- client/src/App.tsx | 16 +++++---- client/src/components/ToolsTab.tsx | 57 +++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 16 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 1d1556e61..9f99511c9 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,7 +1,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { - CallToolResultSchema, + CompatibilityCallToolResultSchema, ClientRequest, CreateMessageRequestSchema, CreateMessageResult, @@ -19,6 +19,7 @@ import { Root, ServerNotification, Tool, + CompatibilityCallToolResult, } from "@modelcontextprotocol/sdk/types.js"; import { useEffect, useRef, useState } from "react"; @@ -44,7 +45,7 @@ import { FolderTree, } from "lucide-react"; -import { AnyZodObject } from "zod"; +import { ZodType } from "zod"; import "./App.css"; import ConsoleTab from "./components/ConsoleTab"; import HistoryAndNotifications from "./components/History"; @@ -69,7 +70,8 @@ const App = () => { const [prompts, setPrompts] = useState([]); const [promptContent, setPromptContent] = useState(""); const [tools, setTools] = useState([]); - const [toolResult, setToolResult] = useState(""); + const [toolResult, setToolResult] = + useState(null); const [error, setError] = useState(null); const [command, setCommand] = useState(() => { return ( @@ -150,7 +152,7 @@ const App = () => { ]); }; - const makeRequest = async ( + const makeRequest = async >( request: ClientRequest, schema: T, ) => { @@ -254,9 +256,9 @@ const App = () => { }, }, }, - CallToolResultSchema, + CompatibilityCallToolResultSchema, ); - setToolResult(JSON.stringify(response.toolResult, null, 2)); + setToolResult(response); }; const handleRootsChange = async () => { @@ -444,7 +446,7 @@ const App = () => { selectedTool={selectedTool} setSelectedTool={(tool) => { setSelectedTool(tool); - setToolResult(""); + setToolResult(null); }} toolResult={toolResult} nextCursor={nextToolCursor} diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index dc6a18f63..333412282 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -8,6 +8,8 @@ import { AlertCircle, Send } from "lucide-react"; import { useState } from "react"; import ListPane from "./ListPane"; +import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js"; + const ToolsTab = ({ tools, listTools, @@ -23,12 +25,56 @@ const ToolsTab = ({ callTool: (name: string, params: Record) => void; selectedTool: Tool | null; setSelectedTool: (tool: Tool) => void; - toolResult: string; + toolResult: CompatibilityCallToolResult | null; nextCursor: ListToolsResult["nextCursor"]; error: string | null; }) => { const [params, setParams] = useState>({}); + const renderToolResult = () => { + if (!toolResult) return null; + + if ("content" in toolResult) { + return ( + <> +

+ Tool Result: {toolResult.isError ? "Error" : "Success"} +

+ {toolResult.content.map((item, index) => ( +
+ {item.type === "text" && ( +
+                  {item.text}
+                
+ )} + {item.type === "image" && ( + Tool result image + )} + {item.type === "resource" && ( +
+                  {JSON.stringify(item.resource, null, 2)}
+                
+ )} +
+ ))} + + ); + } else if ("toolResult" in toolResult) { + return ( + <> +

Tool Result (Legacy):

+
+            {JSON.stringify(toolResult.toolResult, null, 2)}
+          
+ + ); + } + }; + return ( Run Tool - {toolResult && ( - <> -

Tool Result:

-
-                    {toolResult}
-                  
- - )} + {toolResult && renderToolResult()}
) : ( From 76e2cf6fdc9e1f3f12cd7a4f338cc5c4297ee5f1 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Thu, 7 Nov 2024 15:26:39 +0000 Subject: [PATCH 5/9] Add UI for viewing and configuring environment variables --- client/src/App.tsx | 55 +++++++++++++++++++++++++++++++++++++++++++++ server/src/index.ts | 16 ++++++++++--- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 9f99511c9..78d44d154 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -93,6 +93,7 @@ const App = () => { const [mcpClient, setMcpClient] = useState(null); const [notifications, setNotifications] = useState([]); const [roots, setRoots] = useState([]); + const [env, setEnv] = useState>({}); const [pendingSampleRequests, setPendingSampleRequests] = useState< Array< @@ -145,6 +146,15 @@ const App = () => { localStorage.setItem("lastArgs", args); }, [args]); + useEffect(() => { + fetch("http://localhost:3000/default-environment") + .then((response) => response.json()) + .then((data) => setEnv(data)) + .catch((error) => + console.error("Error fetching default environment:", error), + ); + }, []); + const pushHistory = (request: object, response: object) => { setRequestHistory((prev) => [ ...prev, @@ -284,6 +294,7 @@ const App = () => { if (transportType === "stdio") { backendUrl.searchParams.append("command", command); backendUrl.searchParams.append("args", args); + backendUrl.searchParams.append("env", JSON.stringify(env)); } else { backendUrl.searchParams.append("url", url); } @@ -371,6 +382,50 @@ const App = () => { Connect
+ {transportType === "stdio" && ( +
+

+ Environment Variables +

+ {Object.entries(env).map(([key, value]) => ( +
+ + setEnv((prev) => ({ + ...prev, + [e.target.value]: value, + })) + } + /> + + setEnv((prev) => ({ ...prev, [key]: e.target.value })) + } + /> + +
+ ))} + +
+ )} {mcpClient ? ( diff --git a/server/src/index.ts b/server/src/index.ts index f14f9ebf2..58e922bbf 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -2,7 +2,10 @@ import cors from "cors"; import EventSource from "eventsource"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; -import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { + StdioClientTransport, + getDefaultEnvironment, +} from "@modelcontextprotocol/sdk/client/stdio.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import express from "express"; import mcpProxy from "./mcpProxy.js"; @@ -24,8 +27,11 @@ const createTransport = async (query: express.Request["query"]) => { if (transportType === "stdio") { const command = query.command as string; const args = (query.args as string).split(/\s+/); - console.log(`Stdio transport: command=${command}, args=${args}`); - const transport = new StdioClientTransport({ command, args }); + const env = query.env ? JSON.parse(query.env as string) : undefined; + console.log( + `Stdio transport: command=${command}, args=${args}, env=${JSON.stringify(env)}`, + ); + const transport = new StdioClientTransport({ command, args, env }); await transport.start(); console.log("Spawned stdio transport"); return transport; @@ -79,6 +85,10 @@ app.post("/message", async (req, res) => { await transport.handlePostMessage(req, res); }); +app.get("/default-environment", (req, res) => { + res.json(getDefaultEnvironment()); +}); + const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); From afefcb3fa569e55d0dfa5b7464d4f3771a14210a Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Thu, 7 Nov 2024 15:28:10 +0000 Subject: [PATCH 6/9] Make env vars UI toggleable --- client/src/App.tsx | 89 ++++++++++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 35 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 78d44d154..c7d8f817d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -43,6 +43,8 @@ import { Send, Terminal, FolderTree, + ChevronDown, + ChevronRight, } from "lucide-react"; import { ZodType } from "zod"; @@ -94,6 +96,7 @@ const App = () => { const [notifications, setNotifications] = useState([]); const [roots, setRoots] = useState([]); const [env, setEnv] = useState>({}); + const [showEnvVars, setShowEnvVars] = useState(false); const [pendingSampleRequests, setPendingSampleRequests] = useState< Array< @@ -384,46 +387,62 @@ const App = () => { {transportType === "stdio" && (
-

+

- {Object.entries(env).map(([key, value]) => ( -
- - setEnv((prev) => ({ - ...prev, - [e.target.value]: value, - })) - } - /> - - setEnv((prev) => ({ ...prev, [key]: e.target.value })) - } - /> + + {showEnvVars && ( +
+ {Object.entries(env).map(([key, value]) => ( +
+ + setEnv((prev) => ({ + ...prev, + [e.target.value]: value, + })) + } + /> + + setEnv((prev) => ({ + ...prev, + [key]: e.target.value, + })) + } + /> + +
+ ))}
- ))} - + )}
)}
From 5337baa116877739c81a05b70dfad33d658acdd5 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Thu, 7 Nov 2024 15:35:55 +0000 Subject: [PATCH 7/9] Fix type error --- client/src/components/ToolsTab.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 333412282..4c6e09788 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { TabsContent } from "@/components/ui/tabs"; -import { ListToolsResult, Tool } from "@modelcontextprotocol/sdk/types.js"; +import { CallToolResult, ListToolsResult, Tool } from "@modelcontextprotocol/sdk/types.js"; import { AlertCircle, Send } from "lucide-react"; import { useState } from "react"; import ListPane from "./ListPane"; @@ -35,12 +35,14 @@ const ToolsTab = ({ if (!toolResult) return null; if ("content" in toolResult) { + const structuredResult = toolResult as CallToolResult; + return ( <>

- Tool Result: {toolResult.isError ? "Error" : "Success"} + Tool Result: {structuredResult.isError ? "Error" : "Success"}

- {toolResult.content.map((item, index) => ( + {structuredResult.content.map((item, index) => (
{item.type === "text" && (

From 57f0c4915472187aa321e2be06eeae1fffb0929f Mon Sep 17 00:00:00 2001
From: Justin Spahr-Summers 
Date: Fri, 8 Nov 2024 12:00:46 +0000
Subject: [PATCH 8/9] Remove redundant useCallbacks

---
 client/src/components/RootsTab.tsx | 35 ++++++++++++------------------
 1 file changed, 14 insertions(+), 21 deletions(-)

diff --git a/client/src/components/RootsTab.tsx b/client/src/components/RootsTab.tsx
index 2c6f5fb28..33f60d5d9 100644
--- a/client/src/components/RootsTab.tsx
+++ b/client/src/components/RootsTab.tsx
@@ -4,7 +4,6 @@ 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,
@@ -15,31 +14,25 @@ const RootsTab = ({
   setRoots: React.Dispatch>;
   onRootsChange: () => void;
 }) => {
-  const addRoot = useCallback(() => {
+  const addRoot = () => {
     setRoots((currentRoots) => [...currentRoots, { uri: "file://", name: "" }]);
-  }, [setRoots]);
+  };
 
-  const removeRoot = useCallback(
-    (index: number) => {
-      setRoots((currentRoots) => currentRoots.filter((_, i) => i !== index));
-    },
-    [setRoots],
-  );
+  const removeRoot = (index: number) => {
+    setRoots((currentRoots) => currentRoots.filter((_, i) => i !== index));
+  };
 
-  const updateRoot = useCallback(
-    (index: number, field: keyof Root, value: string) => {
-      setRoots((currentRoots) =>
-        currentRoots.map((root, i) =>
-          i === index ? { ...root, [field]: value } : root,
-        ),
-      );
-    },
-    [setRoots],
-  );
+  const updateRoot = (index: number, field: keyof Root, value: string) => {
+    setRoots((currentRoots) =>
+      currentRoots.map((root, i) =>
+        i === index ? { ...root, [field]: value } : root,
+      ),
+    );
+  };
 
-  const handleSave = useCallback(() => {
+  const handleSave = () => {
     onRootsChange();
-  }, [onRootsChange]);
+  };
 
   return (
     

From 2867173e7b2a769ca34e083132a2ea74ff03a3fa Mon Sep 17 00:00:00 2001
From: Justin Spahr-Summers 
Date: Fri, 8 Nov 2024 12:04:45 +0000
Subject: [PATCH 9/9] Record 'roots list changed' notifications in history
 sidebar

---
 client/src/App.tsx                | 32 +++++++++++++++++++++----------
 client/src/components/History.tsx |  2 +-
 2 files changed, 23 insertions(+), 11 deletions(-)

diff --git a/client/src/App.tsx b/client/src/App.tsx
index c7d8f817d..8160fa4ed 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -20,6 +20,7 @@ import {
   ServerNotification,
   Tool,
   CompatibilityCallToolResult,
+  ClientNotification,
 } from "@modelcontextprotocol/sdk/types.js";
 import { useEffect, useRef, useState } from "react";
 
@@ -90,7 +91,7 @@ const App = () => {
   const [url, setUrl] = useState("http://localhost:3001/sse");
   const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio");
   const [requestHistory, setRequestHistory] = useState<
-    { request: string; response: string }[]
+    { request: string; response?: string }[]
   >([]);
   const [mcpClient, setMcpClient] = useState(null);
   const [notifications, setNotifications] = useState([]);
@@ -158,10 +159,13 @@ const App = () => {
       );
   }, []);
 
-  const pushHistory = (request: object, response: object) => {
+  const pushHistory = (request: object, response?: object) => {
     setRequestHistory((prev) => [
       ...prev,
-      { request: JSON.stringify(request), response: JSON.stringify(response) },
+      {
+        request: JSON.stringify(request),
+        response: response !== undefined ? JSON.stringify(response) : undefined,
+      },
     ]);
   };
 
@@ -183,6 +187,20 @@ const App = () => {
     }
   };
 
+  const sendNotification = async (notification: ClientNotification) => {
+    if (!mcpClient) {
+      throw new Error("MCP client not connected");
+    }
+
+    try {
+      await mcpClient.notification(notification);
+      pushHistory(notification);
+    } catch (e: unknown) {
+      setError((e as Error).message);
+      throw e;
+    }
+  };
+
   const listResources = async () => {
     const response = await makeRequest(
       {
@@ -275,13 +293,7 @@ const App = () => {
   };
 
   const handleRootsChange = async () => {
-    if (mcpClient) {
-      try {
-        await mcpClient.sendRootsListChanged();
-      } catch (e) {
-        console.error("Failed to send roots list changed notification:", e);
-      }
-    }
+    sendNotification({ method: "notifications/roots/list_changed" });
   };
 
   const connectMcpServer = async () => {
diff --git a/client/src/components/History.tsx b/client/src/components/History.tsx
index 732f0ab91..832194b8f 100644
--- a/client/src/components/History.tsx
+++ b/client/src/components/History.tsx
@@ -6,7 +6,7 @@ const HistoryAndNotifications = ({
   requestHistory,
   serverNotifications,
 }: {
-  requestHistory: Array<{ request: string; response: string | null }>;
+  requestHistory: Array<{ request: string; response?: string }>;
   serverNotifications: ServerNotification[];
 }) => {
   const [expandedRequests, setExpandedRequests] = useState<{