Merge pull request #18 from modelcontextprotocol/ashwin/progress

Visualize progress notifications
This commit is contained in:
ashwin-ant
2024-10-17 12:05:07 -07:00
committed by GitHub
11 changed files with 2832 additions and 467 deletions

View File

@@ -1,8 +1,5 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {

View File

@@ -10,8 +10,10 @@ import {
Resource,
Tool,
ClientRequest,
ProgressNotificationSchema,
ServerNotification,
} from "mcp-typescript/types.js";
import { useState } from "react";
import { useState, useRef } from "react";
import {
Send,
Bell,
@@ -36,11 +38,11 @@ import ConsoleTab from "./components/ConsoleTab";
import Sidebar from "./components/Sidebar";
import RequestsTab from "./components/RequestsTabs";
import ResourcesTab from "./components/ResourcesTab";
import NotificationsTab from "./components/NotificationsTab";
import PromptsTab, { Prompt } from "./components/PromptsTab";
import ToolsTab from "./components/ToolsTab";
import History from "./components/History";
import { AnyZodObject } from "zod";
import HistoryAndNotifications from "./components/History";
import "./App.css";
const App = () => {
const [connectionStatus, setConnectionStatus] = useState<
@@ -57,7 +59,7 @@ const App = () => {
"/Users/ashwin/.nvm/versions/node/v18.20.4/bin/node",
);
const [args, setArgs] = useState<string>(
"/Users/ashwin/code/example-servers/build/everything/stdio.js",
"/Users/ashwin/code/mcp/example-servers/build/everything/stdio.js",
);
const [url, setUrl] = useState<string>("http://localhost:3001/sse");
const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio");
@@ -65,12 +67,14 @@ const App = () => {
{ request: string; response: string }[]
>([]);
const [mcpClient, setMcpClient] = useState<Client | null>(null);
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
const [selectedResource, setSelectedResource] = useState<Resource | null>(
null,
);
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
const progressTokenRef = useRef(0);
const pushHistory = (request: object, response: object) => {
setRequestHistory((prev) => [
@@ -155,7 +159,13 @@ const App = () => {
const response = await makeRequest(
{
method: "tools/call" as const,
params: { name, arguments: params },
params: {
name,
arguments: params,
_meta: {
progressToken: progressTokenRef.current++,
},
},
},
CallToolResultSchema,
);
@@ -182,6 +192,16 @@ const App = () => {
const clientTransport = new SSEClientTransport(backendUrl);
await client.connect(clientTransport);
client.setNotificationHandler(
ProgressNotificationSchema,
(notification) => {
setNotifications((prevNotifications) => [
...prevNotifications,
notification,
]);
},
);
setMcpClient(client);
setConnectionStatus("connected");
} catch (e) {
@@ -255,10 +275,6 @@ const App = () => {
<Send className="w-4 h-4 mr-2" />
Requests
</TabsTrigger>
<TabsTrigger value="notifications" disabled>
<Bell className="w-4 h-4 mr-2" />
Notifications
</TabsTrigger>
<TabsTrigger value="tools">
<Hammer className="w-4 h-4 mr-2" />
Tools
@@ -279,7 +295,6 @@ const App = () => {
resourceContent={resourceContent}
error={error}
/>
<NotificationsTab />
<PromptsTab
prompts={prompts}
listPrompts={listPrompts}
@@ -315,7 +330,10 @@ const App = () => {
</div>
</div>
</div>
<History requestHistory={requestHistory} />
<HistoryAndNotifications
requestHistory={requestHistory}
serverNotifications={notifications}
/>
</div>
);
};

View File

@@ -1,94 +1,163 @@
import { useState } from "react";
import { Copy } from "lucide-react";
import { ServerNotification } from "mcp-typescript/types.js";
const History = ({
const HistoryAndNotifications = ({
requestHistory,
serverNotifications,
}: {
requestHistory: Array<{ request: string; response: string | null }>;
serverNotifications: ServerNotification[];
}) => {
const [expandedRequests, setExpandedRequests] = useState<{
[key: number]: boolean;
}>({});
const [expandedNotifications, setExpandedNotifications] = useState<{
[key: number]: boolean;
}>({});
const toggleRequestExpansion = (index: number) => {
setExpandedRequests((prev) => ({ ...prev, [index]: !prev[index] }));
};
const toggleNotificationExpansion = (index: number) => {
setExpandedNotifications((prev) => ({ ...prev, [index]: !prev[index] }));
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
return (
<div className="w-64 bg-white shadow-md p-4 overflow-y-auto">
<h2 className="text-lg font-semibold mb-4">History</h2>
<ul className="space-y-3">
{requestHistory
.slice()
.reverse()
.map((request, index) => (
<li
key={index}
className="text-sm text-gray-600 bg-gray-100 p-2 rounded"
>
<div
className="flex justify-between items-center cursor-pointer"
onClick={() =>
toggleRequestExpansion(requestHistory.length - 1 - index)
}
>
<span className="font-mono">
{requestHistory.length - index}.{" "}
{JSON.parse(request.request).method}
</span>
<span>
{expandedRequests[requestHistory.length - 1 - index]
? "▼"
: "▶"}
</span>
</div>
{expandedRequests[requestHistory.length - 1 - index] && (
<>
<div className="mt-2">
<div className="flex justify-between items-center mb-1">
<span className="font-semibold text-blue-600">
Request:
</span>
<button
onClick={() => copyToClipboard(request.request)}
className="text-blue-500 hover:text-blue-700"
>
<Copy size={16} />
</button>
</div>
<pre className="whitespace-pre-wrap break-words bg-blue-50 p-2 rounded">
{JSON.stringify(JSON.parse(request.request), null, 2)}
</pre>
<div className="w-64 bg-white shadow-md p-4 overflow-hidden flex flex-col h-full">
<div className="flex-1 overflow-y-auto mb-4 border-b pb-4">
<h2 className="text-lg font-semibold mb-4">History</h2>
{requestHistory.length === 0 ? (
<p className="text-sm text-gray-500 italic">No history yet</p>
) : (
<ul className="space-y-3">
{requestHistory
.slice()
.reverse()
.map((request, index) => (
<li
key={index}
className="text-sm text-gray-600 bg-gray-100 p-2 rounded"
>
<div
className="flex justify-between items-center cursor-pointer"
onClick={() =>
toggleRequestExpansion(requestHistory.length - 1 - index)
}
>
<span className="font-mono">
{requestHistory.length - index}.{" "}
{JSON.parse(request.request).method}
</span>
<span>
{expandedRequests[requestHistory.length - 1 - index]
? "▼"
: "▶"}
</span>
</div>
{request.response && (
{expandedRequests[requestHistory.length - 1 - index] && (
<>
<div className="mt-2">
<div className="flex justify-between items-center mb-1">
<span className="font-semibold text-blue-600">
Request:
</span>
<button
onClick={() => copyToClipboard(request.request)}
className="text-blue-500 hover:text-blue-700"
>
<Copy size={16} />
</button>
</div>
<pre className="whitespace-pre-wrap break-words bg-blue-50 p-2 rounded">
{JSON.stringify(JSON.parse(request.request), null, 2)}
</pre>
</div>
{request.response && (
<div className="mt-2">
<div className="flex justify-between items-center mb-1">
<span className="font-semibold text-green-600">
Response:
</span>
<button
onClick={() => copyToClipboard(request.response!)}
className="text-blue-500 hover:text-blue-700"
>
<Copy size={16} />
</button>
</div>
<pre className="whitespace-pre-wrap break-words bg-green-50 p-2 rounded">
{JSON.stringify(
JSON.parse(request.response),
null,
2,
)}
</pre>
</div>
)}
</>
)}
</li>
))}
</ul>
)}
</div>
<div className="flex-1 overflow-y-auto">
<h2 className="text-lg font-semibold mb-4">Server Notifications</h2>
{serverNotifications.length === 0 ? (
<p className="text-sm text-gray-500 italic">No notifications yet</p>
) : (
<ul className="space-y-3">
{serverNotifications
.slice()
.reverse()
.map((notification, index) => (
<li
key={index}
className="text-sm text-gray-600 bg-gray-100 p-2 rounded"
>
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => toggleNotificationExpansion(index)}
>
<span className="font-mono">
{serverNotifications.length - index}.{" "}
{notification.method}
</span>
<span>{expandedNotifications[index] ? "▼" : "▶"}</span>
</div>
{expandedNotifications[index] && (
<div className="mt-2">
<div className="flex justify-between items-center mb-1">
<span className="font-semibold text-green-600">
Response:
<span className="font-semibold text-purple-600">
Details:
</span>
<button
onClick={() => copyToClipboard(request.response!)}
onClick={() =>
copyToClipboard(JSON.stringify(notification))
}
className="text-blue-500 hover:text-blue-700"
>
<Copy size={16} />
</button>
</div>
<pre className="whitespace-pre-wrap break-words bg-green-50 p-2 rounded">
{JSON.stringify(JSON.parse(request.response), null, 2)}
<pre className="whitespace-pre-wrap break-words bg-purple-50 p-2 rounded">
{JSON.stringify(notification, null, 2)}
</pre>
</div>
)}
</>
)}
</li>
))}
</ul>
</li>
))}
</ul>
)}
</div>
</div>
);
};
export default History;
export default HistoryAndNotifications;

View File

@@ -1,33 +0,0 @@
import { Bell } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { TabsContent } from "@/components/ui/tabs";
const NotificationsTab = () => (
<TabsContent value="notifications" className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-4">
<div className="flex space-x-2">
<Input placeholder="Notification method" />
<Button>
<Bell className="w-4 h-4 mr-2" />
Send
</Button>
</div>
<Textarea
placeholder="Notification parameters (JSON)"
className="h-64 font-mono"
/>
</div>
<div className="bg-white rounded-lg shadow p-4">
<h3 className="font-semibold mb-4">Recent Notifications</h3>
<div className="space-y-2">
{/* Notification history would go here */}
</div>
</div>
</div>
</TabsContent>
);
export default NotificationsTab;

View File

@@ -36,7 +36,9 @@ const ToolsTab = ({
renderItem={(tool) => (
<>
<span className="flex-1">{tool.name}</span>
<span className="text-sm text-gray-500">{tool.description}</span>
<span className="text-sm text-gray-500 text-right">
{tool.description}
</span>
</>
)}
title="Tools"

View File

@@ -1 +0,0 @@
{"root":["./vite.config.ts"],"version":"5.6.3"}

2916
packages/mcp-typescript/dist/types.d.ts generated vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -10,20 +10,21 @@ export const ProgressTokenSchema = z.union([z.string(), z.number().int()]);
* An opaque token used to represent a cursor for pagination.
*/
export const CursorSchema = z.string();
export const RequestSchema = z.object({
method: z.string(),
params: z.optional(z
const BaseRequestParamsSchema = z
.object({
_meta: z.optional(z
.object({
_meta: z.optional(z
.object({
/**
* If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications.
*/
progressToken: z.optional(ProgressTokenSchema),
})
.passthrough()),
/**
* If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications.
*/
progressToken: z.optional(ProgressTokenSchema),
})
.passthrough()),
})
.passthrough();
export const RequestSchema = z.object({
method: z.string(),
params: z.optional(BaseRequestParamsSchema),
});
export const NotificationSchema = z.object({
method: z.string(),
@@ -182,7 +183,7 @@ export const ClientCapabilitiesSchema = z.object({
*/
export const InitializeRequestSchema = RequestSchema.extend({
method: z.literal("initialize"),
params: z.object({
params: BaseRequestParamsSchema.extend({
/**
* The latest version of the Model Context Protocol that the client supports. The client MAY decide to support older versions as well.
*/
@@ -285,18 +286,18 @@ export const ProgressNotificationSchema = NotificationSchema.extend({
/**
* The progress token which was given in the initial request, used to associate this notification with the request that is proceeding.
*/
progressToken: ProgressTokenSchema,
progressToken: z.optional(ProgressTokenSchema),
}),
});
/* Pagination */
export const PaginatedRequestSchema = RequestSchema.extend({
params: z.optional(z.object({
params: BaseRequestParamsSchema.extend({
/**
* An opaque token representing the current pagination position.
* If provided, the server should return results starting after this cursor.
*/
cursor: z.optional(CursorSchema),
})),
}),
});
export const PaginatedResultSchema = ResultSchema.extend({
/**
@@ -410,7 +411,7 @@ export const ListResourceTemplatesResultSchema = PaginatedResultSchema.extend({
*/
export const ReadResourceRequestSchema = RequestSchema.extend({
method: z.literal("resources/read"),
params: z.object({
params: BaseRequestParamsSchema.extend({
/**
* The URI of the resource to read. The URI can use any protocol; it is up to the server how to interpret it.
*/
@@ -434,7 +435,7 @@ export const ResourceListChangedNotificationSchema = NotificationSchema.extend({
*/
export const SubscribeRequestSchema = RequestSchema.extend({
method: z.literal("resources/subscribe"),
params: z.object({
params: BaseRequestParamsSchema.extend({
/**
* The URI of the resource to subscribe to. The URI can use any protocol; it is up to the server how to interpret it.
*/
@@ -446,7 +447,7 @@ export const SubscribeRequestSchema = RequestSchema.extend({
*/
export const UnsubscribeRequestSchema = RequestSchema.extend({
method: z.literal("resources/unsubscribe"),
params: z.object({
params: BaseRequestParamsSchema.extend({
/**
* The URI of the resource to unsubscribe from.
*/
@@ -517,7 +518,7 @@ export const ListPromptsResultSchema = PaginatedResultSchema.extend({
*/
export const GetPromptRequestSchema = RequestSchema.extend({
method: z.literal("prompts/get"),
params: z.object({
params: BaseRequestParamsSchema.extend({
/**
* The name of the prompt or prompt template.
*/
@@ -588,7 +589,7 @@ export const CallToolResultSchema = ResultSchema.extend({
*/
export const CallToolRequestSchema = RequestSchema.extend({
method: z.literal("tools/call"),
params: z.object({
params: BaseRequestParamsSchema.extend({
name: z.string(),
arguments: z.optional(z.record(z.unknown())),
}),
@@ -609,7 +610,7 @@ export const LoggingLevelSchema = z.enum(["debug", "info", "warning", "error"]);
*/
export const SetLevelRequestSchema = RequestSchema.extend({
method: z.literal("logging/setLevel"),
params: z.object({
params: BaseRequestParamsSchema.extend({
/**
* The level of logging that the client wants to receive from the server. The server should send all logs at this level and higher (i.e., more severe) to the client as notifications/logging/message.
*/
@@ -642,7 +643,7 @@ export const LoggingMessageNotificationSchema = NotificationSchema.extend({
*/
export const CreateMessageRequestSchema = RequestSchema.extend({
method: z.literal("sampling/createMessage"),
params: z.object({
params: BaseRequestParamsSchema.extend({
messages: z.array(SamplingMessageSchema),
/**
* An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt.
@@ -708,7 +709,7 @@ export const PromptReferenceSchema = z.object({
*/
export const CompleteRequestSchema = RequestSchema.extend({
method: z.literal("completion/complete"),
params: z.object({
params: BaseRequestParamsSchema.extend({
ref: z.union([PromptReferenceSchema, ResourceReferenceSchema]),
/**
* The argument's information

File diff suppressed because one or more lines are too long

View File

@@ -15,24 +15,24 @@ export const ProgressTokenSchema = z.union([z.string(), z.number().int()]);
*/
export const CursorSchema = z.string();
const BaseRequestParamsSchema = z
.object({
_meta: z.optional(
z
.object({
/**
* If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications.
*/
progressToken: z.optional(ProgressTokenSchema),
})
.passthrough(),
),
})
.passthrough();
export const RequestSchema = z.object({
method: z.string(),
params: z.optional(
z
.object({
_meta: z.optional(
z
.object({
/**
* If specified, the caller is requesting out-of-band progress notifications for this request (as represented by notifications/progress). The value of this parameter is an opaque token that will be attached to any subsequent notifications. The receiver is not obligated to provide these notifications.
*/
progressToken: z.optional(ProgressTokenSchema),
})
.passthrough(),
),
})
.passthrough(),
),
params: z.optional(BaseRequestParamsSchema),
});
export const NotificationSchema = z.object({
@@ -209,7 +209,7 @@ export const ClientCapabilitiesSchema = z.object({
*/
export const InitializeRequestSchema = RequestSchema.extend({
method: z.literal("initialize"),
params: z.object({
params: BaseRequestParamsSchema.extend({
/**
* The latest version of the Model Context Protocol that the client supports. The client MAY decide to support older versions as well.
*/
@@ -324,21 +324,19 @@ export const ProgressNotificationSchema = NotificationSchema.extend({
/**
* The progress token which was given in the initial request, used to associate this notification with the request that is proceeding.
*/
progressToken: ProgressTokenSchema,
progressToken: z.optional(ProgressTokenSchema),
}),
});
/* Pagination */
export const PaginatedRequestSchema = RequestSchema.extend({
params: z.optional(
z.object({
/**
* An opaque token representing the current pagination position.
* If provided, the server should return results starting after this cursor.
*/
cursor: z.optional(CursorSchema),
}),
),
params: BaseRequestParamsSchema.extend({
/**
* An opaque token representing the current pagination position.
* If provided, the server should return results starting after this cursor.
*/
cursor: z.optional(CursorSchema),
}),
});
export const PaginatedResultSchema = ResultSchema.extend({
@@ -471,7 +469,7 @@ export const ListResourceTemplatesResultSchema = PaginatedResultSchema.extend({
*/
export const ReadResourceRequestSchema = RequestSchema.extend({
method: z.literal("resources/read"),
params: z.object({
params: BaseRequestParamsSchema.extend({
/**
* The URI of the resource to read. The URI can use any protocol; it is up to the server how to interpret it.
*/
@@ -500,7 +498,7 @@ export const ResourceListChangedNotificationSchema = NotificationSchema.extend({
*/
export const SubscribeRequestSchema = RequestSchema.extend({
method: z.literal("resources/subscribe"),
params: z.object({
params: BaseRequestParamsSchema.extend({
/**
* The URI of the resource to subscribe to. The URI can use any protocol; it is up to the server how to interpret it.
*/
@@ -513,7 +511,7 @@ export const SubscribeRequestSchema = RequestSchema.extend({
*/
export const UnsubscribeRequestSchema = RequestSchema.extend({
method: z.literal("resources/unsubscribe"),
params: z.object({
params: BaseRequestParamsSchema.extend({
/**
* The URI of the resource to unsubscribe from.
*/
@@ -590,7 +588,7 @@ export const ListPromptsResultSchema = PaginatedResultSchema.extend({
*/
export const GetPromptRequestSchema = RequestSchema.extend({
method: z.literal("prompts/get"),
params: z.object({
params: BaseRequestParamsSchema.extend({
/**
* The name of the prompt or prompt template.
*/
@@ -668,7 +666,7 @@ export const CallToolResultSchema = ResultSchema.extend({
*/
export const CallToolRequestSchema = RequestSchema.extend({
method: z.literal("tools/call"),
params: z.object({
params: BaseRequestParamsSchema.extend({
name: z.string(),
arguments: z.optional(z.record(z.unknown())),
}),
@@ -692,7 +690,7 @@ export const LoggingLevelSchema = z.enum(["debug", "info", "warning", "error"]);
*/
export const SetLevelRequestSchema = RequestSchema.extend({
method: z.literal("logging/setLevel"),
params: z.object({
params: BaseRequestParamsSchema.extend({
/**
* The level of logging that the client wants to receive from the server. The server should send all logs at this level and higher (i.e., more severe) to the client as notifications/logging/message.
*/
@@ -727,7 +725,7 @@ export const LoggingMessageNotificationSchema = NotificationSchema.extend({
*/
export const CreateMessageRequestSchema = RequestSchema.extend({
method: z.literal("sampling/createMessage"),
params: z.object({
params: BaseRequestParamsSchema.extend({
messages: z.array(SamplingMessageSchema),
/**
* An optional system prompt the server wants to use for sampling. The client MAY modify or omit this prompt.
@@ -797,7 +795,7 @@ export const PromptReferenceSchema = z.object({
*/
export const CompleteRequestSchema = RequestSchema.extend({
method: z.literal("completion/complete"),
params: z.object({
params: BaseRequestParamsSchema.extend({
ref: z.union([PromptReferenceSchema, ResourceReferenceSchema]),
/**
* The argument's information