Skip to content

Commit 5f9d11b

Browse files
authored
Merge pull request #18 from modelcontextprotocol/ashwin/progress
Visualize progress notifications
2 parents 3b4d813 + 247dae5 commit 5f9d11b

File tree

11 files changed

+3039
-674
lines changed

11 files changed

+3039
-674
lines changed

client/src/App.css

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
#root {
2-
max-width: 1280px;
32
margin: 0 auto;
4-
padding: 2rem;
5-
text-align: center;
63
}
74

85
.logo {

client/src/App.tsx

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import {
1010
Resource,
1111
Tool,
1212
ClientRequest,
13+
ProgressNotificationSchema,
14+
ServerNotification,
1315
} from "mcp-typescript/types.js";
14-
import { useState } from "react";
16+
import { useState, useRef } from "react";
1517
import {
1618
Send,
1719
Bell,
@@ -36,11 +38,11 @@ import ConsoleTab from "./components/ConsoleTab";
3638
import Sidebar from "./components/Sidebar";
3739
import RequestsTab from "./components/RequestsTabs";
3840
import ResourcesTab from "./components/ResourcesTab";
39-
import NotificationsTab from "./components/NotificationsTab";
4041
import PromptsTab, { Prompt } from "./components/PromptsTab";
4142
import ToolsTab from "./components/ToolsTab";
42-
import History from "./components/History";
4343
import { AnyZodObject } from "zod";
44+
import HistoryAndNotifications from "./components/History";
45+
import "./App.css";
4446

4547
const App = () => {
4648
const [connectionStatus, setConnectionStatus] = useState<
@@ -57,20 +59,22 @@ const App = () => {
5759
"/Users/ashwin/.nvm/versions/node/v18.20.4/bin/node",
5860
);
5961
const [args, setArgs] = useState<string>(
60-
"/Users/ashwin/code/example-servers/build/everything/stdio.js",
62+
"/Users/ashwin/code/mcp/example-servers/build/everything/stdio.js",
6163
);
6264
const [url, setUrl] = useState<string>("http://localhost:3001/sse");
6365
const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio");
6466
const [requestHistory, setRequestHistory] = useState<
6567
{ request: string; response: string }[]
6668
>([]);
6769
const [mcpClient, setMcpClient] = useState<Client | null>(null);
70+
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
6871

6972
const [selectedResource, setSelectedResource] = useState<Resource | null>(
7073
null,
7174
);
7275
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
7376
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
77+
const progressTokenRef = useRef(0);
7478

7579
const pushHistory = (request: object, response: object) => {
7680
setRequestHistory((prev) => [
@@ -155,7 +159,13 @@ const App = () => {
155159
const response = await makeRequest(
156160
{
157161
method: "tools/call" as const,
158-
params: { name, arguments: params },
162+
params: {
163+
name,
164+
arguments: params,
165+
_meta: {
166+
progressToken: progressTokenRef.current++,
167+
},
168+
},
159169
},
160170
CallToolResultSchema,
161171
);
@@ -182,6 +192,16 @@ const App = () => {
182192
const clientTransport = new SSEClientTransport(backendUrl);
183193
await client.connect(clientTransport);
184194

195+
client.setNotificationHandler(
196+
ProgressNotificationSchema,
197+
(notification) => {
198+
setNotifications((prevNotifications) => [
199+
...prevNotifications,
200+
notification,
201+
]);
202+
},
203+
);
204+
185205
setMcpClient(client);
186206
setConnectionStatus("connected");
187207
} catch (e) {
@@ -255,10 +275,6 @@ const App = () => {
255275
<Send className="w-4 h-4 mr-2" />
256276
Requests
257277
</TabsTrigger>
258-
<TabsTrigger value="notifications" disabled>
259-
<Bell className="w-4 h-4 mr-2" />
260-
Notifications
261-
</TabsTrigger>
262278
<TabsTrigger value="tools">
263279
<Hammer className="w-4 h-4 mr-2" />
264280
Tools
@@ -279,7 +295,6 @@ const App = () => {
279295
resourceContent={resourceContent}
280296
error={error}
281297
/>
282-
<NotificationsTab />
283298
<PromptsTab
284299
prompts={prompts}
285300
listPrompts={listPrompts}
@@ -315,7 +330,10 @@ const App = () => {
315330
</div>
316331
</div>
317332
</div>
318-
<History requestHistory={requestHistory} />
333+
<HistoryAndNotifications
334+
requestHistory={requestHistory}
335+
serverNotifications={notifications}
336+
/>
319337
</div>
320338
);
321339
};

client/src/components/History.tsx

Lines changed: 126 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,163 @@
11
import { useState } from "react";
22
import { Copy } from "lucide-react";
3+
import { ServerNotification } from "mcp-typescript/types.js";
34

4-
const History = ({
5+
const HistoryAndNotifications = ({
56
requestHistory,
7+
serverNotifications,
68
}: {
79
requestHistory: Array<{ request: string; response: string | null }>;
10+
serverNotifications: ServerNotification[];
811
}) => {
912
const [expandedRequests, setExpandedRequests] = useState<{
1013
[key: number]: boolean;
1114
}>({});
15+
const [expandedNotifications, setExpandedNotifications] = useState<{
16+
[key: number]: boolean;
17+
}>({});
1218

1319
const toggleRequestExpansion = (index: number) => {
1420
setExpandedRequests((prev) => ({ ...prev, [index]: !prev[index] }));
1521
};
1622

23+
const toggleNotificationExpansion = (index: number) => {
24+
setExpandedNotifications((prev) => ({ ...prev, [index]: !prev[index] }));
25+
};
26+
1727
const copyToClipboard = (text: string) => {
1828
navigator.clipboard.writeText(text);
1929
};
2030

2131
return (
22-
<div className="w-64 bg-white shadow-md p-4 overflow-y-auto">
23-
<h2 className="text-lg font-semibold mb-4">History</h2>
24-
<ul className="space-y-3">
25-
{requestHistory
26-
.slice()
27-
.reverse()
28-
.map((request, index) => (
29-
<li
30-
key={index}
31-
className="text-sm text-gray-600 bg-gray-100 p-2 rounded"
32-
>
33-
<div
34-
className="flex justify-between items-center cursor-pointer"
35-
onClick={() =>
36-
toggleRequestExpansion(requestHistory.length - 1 - index)
37-
}
38-
>
39-
<span className="font-mono">
40-
{requestHistory.length - index}.{" "}
41-
{JSON.parse(request.request).method}
42-
</span>
43-
<span>
44-
{expandedRequests[requestHistory.length - 1 - index]
45-
? "▼"
46-
: "▶"}
47-
</span>
48-
</div>
49-
{expandedRequests[requestHistory.length - 1 - index] && (
50-
<>
51-
<div className="mt-2">
52-
<div className="flex justify-between items-center mb-1">
53-
<span className="font-semibold text-blue-600">
54-
Request:
55-
</span>
56-
<button
57-
onClick={() => copyToClipboard(request.request)}
58-
className="text-blue-500 hover:text-blue-700"
59-
>
60-
<Copy size={16} />
61-
</button>
62-
</div>
63-
<pre className="whitespace-pre-wrap break-words bg-blue-50 p-2 rounded">
64-
{JSON.stringify(JSON.parse(request.request), null, 2)}
65-
</pre>
32+
<div className="w-64 bg-white shadow-md p-4 overflow-hidden flex flex-col h-full">
33+
<div className="flex-1 overflow-y-auto mb-4 border-b pb-4">
34+
<h2 className="text-lg font-semibold mb-4">History</h2>
35+
{requestHistory.length === 0 ? (
36+
<p className="text-sm text-gray-500 italic">No history yet</p>
37+
) : (
38+
<ul className="space-y-3">
39+
{requestHistory
40+
.slice()
41+
.reverse()
42+
.map((request, index) => (
43+
<li
44+
key={index}
45+
className="text-sm text-gray-600 bg-gray-100 p-2 rounded"
46+
>
47+
<div
48+
className="flex justify-between items-center cursor-pointer"
49+
onClick={() =>
50+
toggleRequestExpansion(requestHistory.length - 1 - index)
51+
}
52+
>
53+
<span className="font-mono">
54+
{requestHistory.length - index}.{" "}
55+
{JSON.parse(request.request).method}
56+
</span>
57+
<span>
58+
{expandedRequests[requestHistory.length - 1 - index]
59+
? "▼"
60+
: "▶"}
61+
</span>
62+
</div>
63+
{expandedRequests[requestHistory.length - 1 - index] && (
64+
<>
65+
<div className="mt-2">
66+
<div className="flex justify-between items-center mb-1">
67+
<span className="font-semibold text-blue-600">
68+
Request:
69+
</span>
70+
<button
71+
onClick={() => copyToClipboard(request.request)}
72+
className="text-blue-500 hover:text-blue-700"
73+
>
74+
<Copy size={16} />
75+
</button>
76+
</div>
77+
<pre className="whitespace-pre-wrap break-words bg-blue-50 p-2 rounded">
78+
{JSON.stringify(JSON.parse(request.request), null, 2)}
79+
</pre>
80+
</div>
81+
{request.response && (
82+
<div className="mt-2">
83+
<div className="flex justify-between items-center mb-1">
84+
<span className="font-semibold text-green-600">
85+
Response:
86+
</span>
87+
<button
88+
onClick={() => copyToClipboard(request.response!)}
89+
className="text-blue-500 hover:text-blue-700"
90+
>
91+
<Copy size={16} />
92+
</button>
93+
</div>
94+
<pre className="whitespace-pre-wrap break-words bg-green-50 p-2 rounded">
95+
{JSON.stringify(
96+
JSON.parse(request.response),
97+
null,
98+
2,
99+
)}
100+
</pre>
101+
</div>
102+
)}
103+
</>
104+
)}
105+
</li>
106+
))}
107+
</ul>
108+
)}
109+
</div>
110+
<div className="flex-1 overflow-y-auto">
111+
<h2 className="text-lg font-semibold mb-4">Server Notifications</h2>
112+
{serverNotifications.length === 0 ? (
113+
<p className="text-sm text-gray-500 italic">No notifications yet</p>
114+
) : (
115+
<ul className="space-y-3">
116+
{serverNotifications
117+
.slice()
118+
.reverse()
119+
.map((notification, index) => (
120+
<li
121+
key={index}
122+
className="text-sm text-gray-600 bg-gray-100 p-2 rounded"
123+
>
124+
<div
125+
className="flex justify-between items-center cursor-pointer"
126+
onClick={() => toggleNotificationExpansion(index)}
127+
>
128+
<span className="font-mono">
129+
{serverNotifications.length - index}.{" "}
130+
{notification.method}
131+
</span>
132+
<span>{expandedNotifications[index] ? "▼" : "▶"}</span>
66133
</div>
67-
{request.response && (
134+
{expandedNotifications[index] && (
68135
<div className="mt-2">
69136
<div className="flex justify-between items-center mb-1">
70-
<span className="font-semibold text-green-600">
71-
Response:
137+
<span className="font-semibold text-purple-600">
138+
Details:
72139
</span>
73140
<button
74-
onClick={() => copyToClipboard(request.response!)}
141+
onClick={() =>
142+
copyToClipboard(JSON.stringify(notification))
143+
}
75144
className="text-blue-500 hover:text-blue-700"
76145
>
77146
<Copy size={16} />
78147
</button>
79148
</div>
80-
<pre className="whitespace-pre-wrap break-words bg-green-50 p-2 rounded">
81-
{JSON.stringify(JSON.parse(request.response), null, 2)}
149+
<pre className="whitespace-pre-wrap break-words bg-purple-50 p-2 rounded">
150+
{JSON.stringify(notification, null, 2)}
82151
</pre>
83152
</div>
84153
)}
85-
</>
86-
)}
87-
</li>
88-
))}
89-
</ul>
154+
</li>
155+
))}
156+
</ul>
157+
)}
158+
</div>
90159
</div>
91160
);
92161
};
93162

94-
export default History;
163+
export default HistoryAndNotifications;

client/src/components/NotificationsTab.tsx

Lines changed: 0 additions & 33 deletions
This file was deleted.

client/src/components/ToolsTab.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ const ToolsTab = ({
3636
renderItem={(tool) => (
3737
<>
3838
<span className="flex-1">{tool.name}</span>
39-
<span className="text-sm text-gray-500">{tool.description}</span>
39+
<span className="text-sm text-gray-500 text-right">
40+
{tool.description}
41+
</span>
4042
</>
4143
)}
4244
title="Tools"

client/tsconfig.node.tsbuildinfo

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)