Skip to content

Commit 5da4174

Browse files
committed
Add tab and approval flow for server -> client sampling
1 parent b4c70ed commit 5da4174

File tree

2 files changed

+115
-0
lines changed

2 files changed

+115
-0
lines changed

client/src/App.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ import {
1313
ProgressNotificationSchema,
1414
ServerNotification,
1515
EmptyResultSchema,
16+
CreateMessageRequest,
17+
CreateMessageResult,
18+
CreateMessageRequestSchema,
1619
} from "mcp-typescript/types.js";
1720
import { useState, useRef, useEffect } from "react";
21+
1822
import {
1923
Send,
2024
Terminal,
@@ -23,6 +27,7 @@ import {
2327
MessageSquare,
2428
Hammer,
2529
Play,
30+
Hash,
2631
} from "lucide-react";
2732
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
2833
import { Input } from "@/components/ui/input";
@@ -45,6 +50,7 @@ import { AnyZodObject } from "zod";
4550
import HistoryAndNotifications from "./components/History";
4651
import "./App.css";
4752
import PingTab from "./components/PingTab";
53+
import SamplingTab, { PendingRequest } from "./components/SamplingTab";
4854

4955
const App = () => {
5056
const [connectionStatus, setConnectionStatus] = useState<
@@ -77,6 +83,32 @@ const App = () => {
7783
const [mcpClient, setMcpClient] = useState<Client | null>(null);
7884
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
7985

86+
const [pendingSampleRequests, setPendingSampleRequests] = useState<
87+
Array<
88+
PendingRequest & {
89+
resolve: (result: CreateMessageResult) => void;
90+
reject: (error: Error) => void;
91+
}
92+
>
93+
>([]);
94+
const nextRequestId = useRef(0);
95+
96+
const handleApproveSampling = (id: number, result: CreateMessageResult) => {
97+
setPendingSampleRequests((prev) => {
98+
const request = prev.find((r) => r.id === id);
99+
request?.resolve(result);
100+
return prev.filter((r) => r.id !== id);
101+
});
102+
};
103+
104+
const handleRejectSampling = (id: number) => {
105+
setPendingSampleRequests((prev) => {
106+
const request = prev.find((r) => r.id === id);
107+
request?.reject(new Error("Sampling request rejected"));
108+
return prev.filter((r) => r.id !== id);
109+
});
110+
};
111+
80112
const [selectedResource, setSelectedResource] = useState<Resource | null>(
81113
null,
82114
);
@@ -229,6 +261,15 @@ const App = () => {
229261
},
230262
);
231263

264+
client.setRequestHandler(CreateMessageRequestSchema, (request) => {
265+
return new Promise<CreateMessageResult>((resolve, reject) => {
266+
setPendingSampleRequests((prev) => [
267+
...prev,
268+
{ id: nextRequestId.current++, request, resolve, reject },
269+
]);
270+
});
271+
});
272+
232273
setMcpClient(client);
233274
setConnectionStatus("connected");
234275
} catch (e) {
@@ -314,6 +355,10 @@ const App = () => {
314355
<Bell className="w-4 h-4 mr-2" />
315356
Ping
316357
</TabsTrigger>
358+
<TabsTrigger value="sampling">
359+
<Hash className="w-4 h-4 mr-2" />
360+
Sampling
361+
</TabsTrigger>
317362
</TabsList>
318363

319364
<div className="w-full">
@@ -362,6 +407,11 @@ const App = () => {
362407
);
363408
}}
364409
/>
410+
<SamplingTab
411+
pendingRequests={pendingSampleRequests}
412+
onApprove={handleApproveSampling}
413+
onReject={handleRejectSampling}
414+
/>
365415
</div>
366416
</Tabs>
367417
) : (
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { TabsContent } from "@/components/ui/tabs";
2+
import { Alert, AlertDescription } from "@/components/ui/alert";
3+
import { Button } from "@/components/ui/button";
4+
import {
5+
CreateMessageRequest,
6+
CreateMessageResult,
7+
} from "mcp-typescript/types.js";
8+
9+
export type PendingRequest = {
10+
id: number;
11+
request: CreateMessageRequest;
12+
};
13+
14+
export type Props = {
15+
pendingRequests: PendingRequest[];
16+
onApprove: (id: number, result: CreateMessageResult) => void;
17+
onReject: (id: number) => void;
18+
};
19+
20+
const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {
21+
const handleApprove = (id: number) => {
22+
// For now, just return a stub response
23+
onApprove(id, {
24+
model: "stub-model",
25+
stopReason: "endTurn",
26+
role: "assistant",
27+
content: {
28+
type: "text",
29+
text: "This is a stub response.",
30+
},
31+
});
32+
};
33+
34+
return (
35+
<TabsContent value="sampling" className="h-96">
36+
<Alert>
37+
<AlertDescription>
38+
When the server requests LLM sampling, requests will appear here for
39+
approval.
40+
</AlertDescription>
41+
</Alert>
42+
<div className="mt-4 space-y-4">
43+
<h3 className="text-lg font-semibold">Recent Requests</h3>
44+
{pendingRequests.map((request) => (
45+
<div key={request.id} className="p-4 border rounded-lg space-y-4">
46+
<pre className="bg-gray-50 p-2 rounded">
47+
{JSON.stringify(request.request, null, 2)}
48+
</pre>
49+
<div className="flex space-x-2">
50+
<Button onClick={() => handleApprove(request.id)}>Approve</Button>
51+
<Button variant="outline" onClick={() => onReject(request.id)}>
52+
Reject
53+
</Button>
54+
</div>
55+
</div>
56+
))}
57+
{pendingRequests.length === 0 && (
58+
<p className="text-gray-500">No pending requests</p>
59+
)}
60+
</div>
61+
</TabsContent>
62+
);
63+
};
64+
65+
export default SamplingTab;

0 commit comments

Comments
 (0)