Add tab and approval flow for server -> client sampling

This commit is contained in:
Justin Spahr-Summers
2024-10-25 14:47:08 +01:00
parent b4c70edb78
commit 5da417470f
2 changed files with 115 additions and 0 deletions

View File

@@ -13,8 +13,12 @@ import {
ProgressNotificationSchema, ProgressNotificationSchema,
ServerNotification, ServerNotification,
EmptyResultSchema, EmptyResultSchema,
CreateMessageRequest,
CreateMessageResult,
CreateMessageRequestSchema,
} from "mcp-typescript/types.js"; } from "mcp-typescript/types.js";
import { useState, useRef, useEffect } from "react"; import { useState, useRef, useEffect } from "react";
import { import {
Send, Send,
Terminal, Terminal,
@@ -23,6 +27,7 @@ import {
MessageSquare, MessageSquare,
Hammer, Hammer,
Play, Play,
Hash,
} from "lucide-react"; } from "lucide-react";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -45,6 +50,7 @@ import { AnyZodObject } from "zod";
import HistoryAndNotifications from "./components/History"; import HistoryAndNotifications from "./components/History";
import "./App.css"; import "./App.css";
import PingTab from "./components/PingTab"; import PingTab from "./components/PingTab";
import SamplingTab, { PendingRequest } from "./components/SamplingTab";
const App = () => { const App = () => {
const [connectionStatus, setConnectionStatus] = useState< const [connectionStatus, setConnectionStatus] = useState<
@@ -77,6 +83,32 @@ const App = () => {
const [mcpClient, setMcpClient] = useState<Client | null>(null); const [mcpClient, setMcpClient] = useState<Client | null>(null);
const [notifications, setNotifications] = useState<ServerNotification[]>([]); const [notifications, setNotifications] = useState<ServerNotification[]>([]);
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<Resource | null>( const [selectedResource, setSelectedResource] = useState<Resource | null>(
null, null,
); );
@@ -229,6 +261,15 @@ const App = () => {
}, },
); );
client.setRequestHandler(CreateMessageRequestSchema, (request) => {
return new Promise<CreateMessageResult>((resolve, reject) => {
setPendingSampleRequests((prev) => [
...prev,
{ id: nextRequestId.current++, request, resolve, reject },
]);
});
});
setMcpClient(client); setMcpClient(client);
setConnectionStatus("connected"); setConnectionStatus("connected");
} catch (e) { } catch (e) {
@@ -314,6 +355,10 @@ const App = () => {
<Bell className="w-4 h-4 mr-2" /> <Bell className="w-4 h-4 mr-2" />
Ping Ping
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="sampling">
<Hash className="w-4 h-4 mr-2" />
Sampling
</TabsTrigger>
</TabsList> </TabsList>
<div className="w-full"> <div className="w-full">
@@ -362,6 +407,11 @@ const App = () => {
); );
}} }}
/> />
<SamplingTab
pendingRequests={pendingSampleRequests}
onApprove={handleApproveSampling}
onReject={handleRejectSampling}
/>
</div> </div>
</Tabs> </Tabs>
) : ( ) : (

View File

@@ -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 (
<TabsContent value="sampling" className="h-96">
<Alert>
<AlertDescription>
When the server requests LLM sampling, requests will appear here for
approval.
</AlertDescription>
</Alert>
<div className="mt-4 space-y-4">
<h3 className="text-lg font-semibold">Recent Requests</h3>
{pendingRequests.map((request) => (
<div key={request.id} className="p-4 border rounded-lg space-y-4">
<pre className="bg-gray-50 p-2 rounded">
{JSON.stringify(request.request, null, 2)}
</pre>
<div className="flex space-x-2">
<Button onClick={() => handleApprove(request.id)}>Approve</Button>
<Button variant="outline" onClick={() => onReject(request.id)}>
Reject
</Button>
</div>
</div>
))}
{pendingRequests.length === 0 && (
<p className="text-gray-500">No pending requests</p>
)}
</div>
</TabsContent>
);
};
export default SamplingTab;