From 5da417470fd1487ca38cccc0af310bde2e734de7 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Fri, 25 Oct 2024 14:47:08 +0100 Subject: [PATCH 1/3] Add tab and approval flow for server -> client sampling --- client/src/App.tsx | 50 +++++++++++++++++++++ client/src/components/SamplingTab.tsx | 65 +++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 client/src/components/SamplingTab.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index c0c5b81..15cdff8 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -13,8 +13,12 @@ import { ProgressNotificationSchema, ServerNotification, EmptyResultSchema, + CreateMessageRequest, + CreateMessageResult, + CreateMessageRequestSchema, } from "mcp-typescript/types.js"; import { useState, useRef, useEffect } from "react"; + import { Send, Terminal, @@ -23,6 +27,7 @@ import { MessageSquare, Hammer, Play, + Hash, } from "lucide-react"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Input } from "@/components/ui/input"; @@ -45,6 +50,7 @@ import { AnyZodObject } from "zod"; import HistoryAndNotifications from "./components/History"; import "./App.css"; import PingTab from "./components/PingTab"; +import SamplingTab, { PendingRequest } from "./components/SamplingTab"; const App = () => { const [connectionStatus, setConnectionStatus] = useState< @@ -77,6 +83,32 @@ const App = () => { const [mcpClient, setMcpClient] = useState(null); const [notifications, setNotifications] = useState([]); + const [pendingSampleRequests, setPendingSampleRequests] = useState< + Array< + PendingRequest & { + resolve: (result: CreateMessageResult) => void; + reject: (error: Error) => void; + } + > + >([]); + const nextRequestId = useRef(0); + + const handleApproveSampling = (id: number, result: CreateMessageResult) => { + setPendingSampleRequests((prev) => { + const request = prev.find((r) => r.id === id); + request?.resolve(result); + return prev.filter((r) => r.id !== id); + }); + }; + + const handleRejectSampling = (id: number) => { + setPendingSampleRequests((prev) => { + const request = prev.find((r) => r.id === id); + request?.reject(new Error("Sampling request rejected")); + return prev.filter((r) => r.id !== id); + }); + }; + const [selectedResource, setSelectedResource] = useState( null, ); @@ -229,6 +261,15 @@ const App = () => { }, ); + client.setRequestHandler(CreateMessageRequestSchema, (request) => { + return new Promise((resolve, reject) => { + setPendingSampleRequests((prev) => [ + ...prev, + { id: nextRequestId.current++, request, resolve, reject }, + ]); + }); + }); + setMcpClient(client); setConnectionStatus("connected"); } catch (e) { @@ -314,6 +355,10 @@ const App = () => { Ping + + + Sampling +
@@ -362,6 +407,11 @@ const App = () => { ); }} /> +
) : ( diff --git a/client/src/components/SamplingTab.tsx b/client/src/components/SamplingTab.tsx new file mode 100644 index 0000000..372d3fd --- /dev/null +++ b/client/src/components/SamplingTab.tsx @@ -0,0 +1,65 @@ +import { TabsContent } from "@/components/ui/tabs"; +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { + CreateMessageRequest, + CreateMessageResult, +} from "mcp-typescript/types.js"; + +export type PendingRequest = { + id: number; + request: CreateMessageRequest; +}; + +export type Props = { + pendingRequests: PendingRequest[]; + onApprove: (id: number, result: CreateMessageResult) => void; + onReject: (id: number) => void; +}; + +const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => { + const handleApprove = (id: number) => { + // For now, just return a stub response + onApprove(id, { + model: "stub-model", + stopReason: "endTurn", + role: "assistant", + content: { + type: "text", + text: "This is a stub response.", + }, + }); + }; + + return ( + + + + When the server requests LLM sampling, requests will appear here for + approval. + + +
+

Recent Requests

+ {pendingRequests.map((request) => ( +
+
+              {JSON.stringify(request.request, null, 2)}
+            
+
+ + +
+
+ ))} + {pendingRequests.length === 0 && ( +

No pending requests

+ )} +
+
+ ); +}; + +export default SamplingTab; From 0d668677011bac01ff7ee91952e55b8de7bed148 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Fri, 25 Oct 2024 14:48:15 +0100 Subject: [PATCH 2/3] Add notification badge --- client/src/App.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 15cdff8..beb6625 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -13,7 +13,6 @@ import { ProgressNotificationSchema, ServerNotification, EmptyResultSchema, - CreateMessageRequest, CreateMessageResult, CreateMessageRequestSchema, } from "mcp-typescript/types.js"; @@ -355,9 +354,14 @@ const App = () => { Ping - + Sampling + {pendingSampleRequests.length > 0 && ( + + {pendingSampleRequests.length} + + )} From 7926eea39c09d543125efbb5512f9094175bf5bc Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Mon, 28 Oct 2024 13:01:31 +0000 Subject: [PATCH 3/3] Fix import --- client/src/components/SamplingTab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/components/SamplingTab.tsx b/client/src/components/SamplingTab.tsx index 372d3fd..d851880 100644 --- a/client/src/components/SamplingTab.tsx +++ b/client/src/components/SamplingTab.tsx @@ -1,10 +1,10 @@ -import { TabsContent } from "@/components/ui/tabs"; import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; +import { TabsContent } from "@/components/ui/tabs"; import { CreateMessageRequest, CreateMessageResult, -} from "mcp-typescript/types.js"; +} from "@modelcontextprotocol/sdk/types.js"; export type PendingRequest = { id: number;