diff --git a/client/src/App.tsx b/client/src/App.tsx index f65d1e5..055552a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,18 +1,10 @@ -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 { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { CallToolResultSchema, ClientRequest, + CreateMessageRequestSchema, + CreateMessageResult, EmptyResultSchema, GetPromptResultSchema, ListPromptsResultSchema, @@ -24,16 +16,28 @@ import { ServerNotification, Tool, } from "@modelcontextprotocol/sdk/types.js"; +import { useEffect, useRef, useState } from "react"; + +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 { Bell, Files, Hammer, + Hash, MessageSquare, Play, Send, Terminal, } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; import { AnyZodObject } from "zod"; import "./App.css"; @@ -43,6 +47,7 @@ import PingTab from "./components/PingTab"; import PromptsTab, { Prompt } from "./components/PromptsTab"; import RequestsTab from "./components/RequestsTabs"; import ResourcesTab from "./components/ResourcesTab"; +import SamplingTab, { PendingRequest } from "./components/SamplingTab"; import Sidebar from "./components/Sidebar"; import ToolsTab from "./components/ToolsTab"; @@ -77,6 +82,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 +260,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 +354,15 @@ const App = () => { Ping + + + Sampling + {pendingSampleRequests.length > 0 && ( + + {pendingSampleRequests.length} + + )} +
@@ -362,6 +411,11 @@ const App = () => { ); }} /> +
) : ( diff --git a/client/src/components/SamplingTab.tsx b/client/src/components/SamplingTab.tsx new file mode 100644 index 0000000..d851880 --- /dev/null +++ b/client/src/components/SamplingTab.tsx @@ -0,0 +1,65 @@ +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { TabsContent } from "@/components/ui/tabs"; +import { + CreateMessageRequest, + CreateMessageResult, +} from "@modelcontextprotocol/sdk/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;