Skip to content

Commit a5606f7

Browse files
Merge pull request modelcontextprotocol#28 from modelcontextprotocol/justin/sampling
Add tab and approval flow for server -> client sampling
2 parents 8ff82d7 + 7926eea commit a5606f7

File tree

2 files changed

+130
-11
lines changed

2 files changed

+130
-11
lines changed

client/src/App.tsx

Lines changed: 65 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,10 @@
1-
import { Button } from "@/components/ui/button";
2-
import { Input } from "@/components/ui/input";
3-
import {
4-
Select,
5-
SelectContent,
6-
SelectItem,
7-
SelectTrigger,
8-
SelectValue,
9-
} from "@/components/ui/select";
10-
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
111
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
122
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
133
import {
144
CallToolResultSchema,
155
ClientRequest,
6+
CreateMessageRequestSchema,
7+
CreateMessageResult,
168
EmptyResultSchema,
179
GetPromptResultSchema,
1810
ListPromptsResultSchema,
@@ -24,16 +16,28 @@ import {
2416
ServerNotification,
2517
Tool,
2618
} from "@modelcontextprotocol/sdk/types.js";
19+
import { useEffect, useRef, useState } from "react";
20+
21+
import { Button } from "@/components/ui/button";
22+
import { Input } from "@/components/ui/input";
23+
import {
24+
Select,
25+
SelectContent,
26+
SelectItem,
27+
SelectTrigger,
28+
SelectValue,
29+
} from "@/components/ui/select";
30+
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
2731
import {
2832
Bell,
2933
Files,
3034
Hammer,
35+
Hash,
3136
MessageSquare,
3237
Play,
3338
Send,
3439
Terminal,
3540
} from "lucide-react";
36-
import { useEffect, useRef, useState } from "react";
3741

3842
import { AnyZodObject } from "zod";
3943
import "./App.css";
@@ -43,6 +47,7 @@ import PingTab from "./components/PingTab";
4347
import PromptsTab, { Prompt } from "./components/PromptsTab";
4448
import RequestsTab from "./components/RequestsTabs";
4549
import ResourcesTab from "./components/ResourcesTab";
50+
import SamplingTab, { PendingRequest } from "./components/SamplingTab";
4651
import Sidebar from "./components/Sidebar";
4752
import ToolsTab from "./components/ToolsTab";
4853

@@ -77,6 +82,32 @@ const App = () => {
7782
const [mcpClient, setMcpClient] = useState<Client | null>(null);
7883
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
7984

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

263+
client.setRequestHandler(CreateMessageRequestSchema, (request) => {
264+
return new Promise<CreateMessageResult>((resolve, reject) => {
265+
setPendingSampleRequests((prev) => [
266+
...prev,
267+
{ id: nextRequestId.current++, request, resolve, reject },
268+
]);
269+
});
270+
});
271+
232272
setMcpClient(client);
233273
setConnectionStatus("connected");
234274
} catch (e) {
@@ -314,6 +354,15 @@ const App = () => {
314354
<Bell className="w-4 h-4 mr-2" />
315355
Ping
316356
</TabsTrigger>
357+
<TabsTrigger value="sampling" className="relative">
358+
<Hash className="w-4 h-4 mr-2" />
359+
Sampling
360+
{pendingSampleRequests.length > 0 && (
361+
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
362+
{pendingSampleRequests.length}
363+
</span>
364+
)}
365+
</TabsTrigger>
317366
</TabsList>
318367

319368
<div className="w-full">
@@ -362,6 +411,11 @@ const App = () => {
362411
);
363412
}}
364413
/>
414+
<SamplingTab
415+
pendingRequests={pendingSampleRequests}
416+
onApprove={handleApproveSampling}
417+
onReject={handleRejectSampling}
418+
/>
365419
</div>
366420
</Tabs>
367421
) : (
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Alert, AlertDescription } from "@/components/ui/alert";
2+
import { Button } from "@/components/ui/button";
3+
import { TabsContent } from "@/components/ui/tabs";
4+
import {
5+
CreateMessageRequest,
6+
CreateMessageResult,
7+
} from "@modelcontextprotocol/sdk/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)