From 48355e9cb2f14f5418267328044ab1ec21d22d68 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Wed, 16 Oct 2024 14:44:21 -0700 Subject: [PATCH] receive and display server notifications --- client/src/App.css | 3 - client/src/App.tsx | 40 +++-- client/src/components/History.tsx | 183 ++++++++++++++------- client/src/components/NotificationsTab.tsx | 33 ---- client/src/components/ToolsTab.tsx | 4 +- client/tsconfig.node.tsbuildinfo | 1 - 6 files changed, 158 insertions(+), 106 deletions(-) delete mode 100644 client/src/components/NotificationsTab.tsx delete mode 100644 client/tsconfig.node.tsbuildinfo diff --git a/client/src/App.css b/client/src/App.css index b9d355d..5f47310 100644 --- a/client/src/App.css +++ b/client/src/App.css @@ -1,8 +1,5 @@ #root { - max-width: 1280px; margin: 0 auto; - padding: 2rem; - text-align: center; } .logo { diff --git a/client/src/App.tsx b/client/src/App.tsx index 5747b9d..b255ab5 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -10,8 +10,10 @@ import { Resource, Tool, ClientRequest, + ProgressNotificationSchema, + ServerNotification, } from "mcp-typescript/types.js"; -import { useState } from "react"; +import { useState, useRef } from "react"; import { Send, Bell, @@ -36,11 +38,11 @@ import ConsoleTab from "./components/ConsoleTab"; import Sidebar from "./components/Sidebar"; import RequestsTab from "./components/RequestsTabs"; import ResourcesTab from "./components/ResourcesTab"; -import NotificationsTab from "./components/NotificationsTab"; import PromptsTab, { Prompt } from "./components/PromptsTab"; import ToolsTab from "./components/ToolsTab"; -import History from "./components/History"; import { AnyZodObject } from "zod"; +import HistoryAndNotifications from "./components/History"; +import "./App.css"; const App = () => { const [connectionStatus, setConnectionStatus] = useState< @@ -57,7 +59,7 @@ const App = () => { "/Users/ashwin/.nvm/versions/node/v18.20.4/bin/node", ); const [args, setArgs] = useState( - "/Users/ashwin/code/example-servers/build/everything/stdio.js", + "/Users/ashwin/code/mcp/example-servers/build/everything/stdio.js", ); const [url, setUrl] = useState("http://localhost:3001/sse"); const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio"); @@ -65,12 +67,14 @@ const App = () => { { request: string; response: string }[] >([]); const [mcpClient, setMcpClient] = useState(null); + const [notifications, setNotifications] = useState([]); const [selectedResource, setSelectedResource] = useState( null, ); const [selectedPrompt, setSelectedPrompt] = useState(null); const [selectedTool, setSelectedTool] = useState(null); + const progressTokenRef = useRef(0); const pushHistory = (request: object, response: object) => { setRequestHistory((prev) => [ @@ -155,7 +159,13 @@ const App = () => { const response = await makeRequest( { method: "tools/call" as const, - params: { name, arguments: params }, + params: { + name, + arguments: params, + _meta: { + progressToken: progressTokenRef.current++, + }, + }, }, CallToolResultSchema, ); @@ -182,6 +192,16 @@ const App = () => { const clientTransport = new SSEClientTransport(backendUrl); await client.connect(clientTransport); + client.setNotificationHandler( + ProgressNotificationSchema, + (notification) => { + setNotifications((prevNotifications) => [ + ...prevNotifications, + notification, + ]); + }, + ); + setMcpClient(client); setConnectionStatus("connected"); } catch (e) { @@ -255,10 +275,6 @@ const App = () => { Requests - - - Notifications - Tools @@ -279,7 +295,6 @@ const App = () => { resourceContent={resourceContent} error={error} /> - { - + ); }; diff --git a/client/src/components/History.tsx b/client/src/components/History.tsx index c0ac2d1..066fbc1 100644 --- a/client/src/components/History.tsx +++ b/client/src/components/History.tsx @@ -1,94 +1,163 @@ import { useState } from "react"; import { Copy } from "lucide-react"; +import { ServerNotification } from "mcp-typescript/types.js"; -const History = ({ +const HistoryAndNotifications = ({ requestHistory, + serverNotifications, }: { requestHistory: Array<{ request: string; response: string | null }>; + serverNotifications: ServerNotification[]; }) => { const [expandedRequests, setExpandedRequests] = useState<{ [key: number]: boolean; }>({}); + const [expandedNotifications, setExpandedNotifications] = useState<{ + [key: number]: boolean; + }>({}); const toggleRequestExpansion = (index: number) => { setExpandedRequests((prev) => ({ ...prev, [index]: !prev[index] })); }; + const toggleNotificationExpansion = (index: number) => { + setExpandedNotifications((prev) => ({ ...prev, [index]: !prev[index] })); + }; + const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); }; return ( -
-

History

-
    - {requestHistory - .slice() - .reverse() - .map((request, index) => ( -
  • -
    - toggleRequestExpansion(requestHistory.length - 1 - index) - } - > - - {requestHistory.length - index}.{" "} - {JSON.parse(request.request).method} - - - {expandedRequests[requestHistory.length - 1 - index] - ? "▼" - : "▶"} - -
    - {expandedRequests[requestHistory.length - 1 - index] && ( - <> -
    -
    - - Request: - - -
    -
    -                      {JSON.stringify(JSON.parse(request.request), null, 2)}
    -                    
    +
    +
    +

    History

    + {requestHistory.length === 0 ? ( +

    No history yet

    + ) : ( +
      + {requestHistory + .slice() + .reverse() + .map((request, index) => ( +
    • +
      + toggleRequestExpansion(requestHistory.length - 1 - index) + } + > + + {requestHistory.length - index}.{" "} + {JSON.parse(request.request).method} + + + {expandedRequests[requestHistory.length - 1 - index] + ? "▼" + : "▶"} +
      - {request.response && ( + {expandedRequests[requestHistory.length - 1 - index] && ( + <> +
      +
      + + Request: + + +
      +
      +                          {JSON.stringify(JSON.parse(request.request), null, 2)}
      +                        
      +
      + {request.response && ( +
      +
      + + Response: + + +
      +
      +                            {JSON.stringify(
      +                              JSON.parse(request.response),
      +                              null,
      +                              2,
      +                            )}
      +                          
      +
      + )} + + )} +
    • + ))} +
    + )} +
    +
    +

    Server Notifications

    + {serverNotifications.length === 0 ? ( +

    No notifications yet

    + ) : ( +
      + {serverNotifications + .slice() + .reverse() + .map((notification, index) => ( +
    • +
      toggleNotificationExpansion(index)} + > + + {serverNotifications.length - index}.{" "} + {notification.method} + + {expandedNotifications[index] ? "▼" : "▶"} +
      + {expandedNotifications[index] && (
      - - Response: + + Details:
      -
      -                        {JSON.stringify(JSON.parse(request.response), null, 2)}
      +                      
      +                        {JSON.stringify(notification, null, 2)}
                             
      )} - - )} -
    • - ))} -
    +
  • + ))} +
+ )} +
); }; -export default History; +export default HistoryAndNotifications; diff --git a/client/src/components/NotificationsTab.tsx b/client/src/components/NotificationsTab.tsx deleted file mode 100644 index 7e23b99..0000000 --- a/client/src/components/NotificationsTab.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Bell } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import { TabsContent } from "@/components/ui/tabs"; - -const NotificationsTab = () => ( - -
-
-
- - -
-