Skip to content

Commit 897e637

Browse files
authored
Merge pull request modelcontextprotocol#244 from NicolasMontone/feat/new-alert-and-copy-button
feat: add new toast and add copy button to JSON.
2 parents f9cb2c1 + 5db5fc2 commit 897e637

File tree

16 files changed

+706
-127
lines changed

16 files changed

+706
-127
lines changed

client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@radix-ui/react-select": "^2.1.2",
3333
"@radix-ui/react-slot": "^1.1.0",
3434
"@radix-ui/react-tabs": "^1.1.1",
35+
"@radix-ui/react-toast": "^1.2.6",
3536
"@types/prismjs": "^1.26.5",
3637
"class-variance-authority": "^0.7.0",
3738
"clsx": "^2.1.1",
@@ -42,7 +43,6 @@
4243
"react": "^18.3.1",
4344
"react-dom": "^18.3.1",
4445
"react-simple-code-editor": "^0.14.1",
45-
"react-toastify": "^10.0.6",
4646
"serve-handler": "^6.1.6",
4747
"tailwind-merge": "^2.5.3",
4848
"tailwindcss-animate": "^1.0.7",

client/src/App.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import {
3333
MessageSquare,
3434
} from "lucide-react";
3535

36-
import { toast } from "react-toastify";
3736
import { z } from "zod";
3837
import "./App.css";
3938
import ConsoleTab from "./components/ConsoleTab";
@@ -47,13 +46,14 @@ import Sidebar from "./components/Sidebar";
4746
import ToolsTab from "./components/ToolsTab";
4847
import { DEFAULT_INSPECTOR_CONFIG } from "./lib/constants";
4948
import { InspectorConfig } from "./lib/configurationTypes";
50-
49+
import { useToast } from "@/hooks/use-toast";
5150
const params = new URLSearchParams(window.location.search);
5251
const PROXY_PORT = params.get("proxyPort") ?? "6277";
5352
const PROXY_SERVER_URL = `http://${window.location.hostname}:${PROXY_PORT}`;
5453
const CONFIG_LOCAL_STORAGE_KEY = "inspectorConfig_v1";
5554

5655
const App = () => {
56+
const { toast } = useToast();
5757
// Handle OAuth callback route
5858
const [resources, setResources] = useState<Resource[]>([]);
5959
const [resourceTemplates, setResourceTemplates] = useState<
@@ -208,11 +208,14 @@ const App = () => {
208208
newUrl.searchParams.delete("serverUrl");
209209
window.history.replaceState({}, "", newUrl.toString());
210210
// Show success toast for OAuth
211-
toast.success("Successfully authenticated with OAuth");
211+
toast({
212+
title: "Success",
213+
description: "Successfully authenticated with OAuth",
214+
});
212215
// Connect to the server
213216
connectMcpServer();
214217
}
215-
}, [connectMcpServer]);
218+
}, [connectMcpServer, toast]);
216219

217220
useEffect(() => {
218221
fetch(`${PROXY_SERVER_URL}/config`)

client/src/components/History.tsx

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { ServerNotification } from "@modelcontextprotocol/sdk/types.js";
2-
import { Copy } from "lucide-react";
32
import { useState } from "react";
43
import JsonView from "./JsonView";
54

@@ -25,10 +24,6 @@ const HistoryAndNotifications = ({
2524
setExpandedNotifications((prev) => ({ ...prev, [index]: !prev[index] }));
2625
};
2726

28-
const copyToClipboard = (text: string) => {
29-
navigator.clipboard.writeText(text);
30-
};
31-
3227
return (
3328
<div className="bg-card overflow-hidden flex h-full">
3429
<div className="flex-1 overflow-y-auto p-4 border-r">
@@ -68,33 +63,24 @@ const HistoryAndNotifications = ({
6863
<span className="font-semibold text-blue-600">
6964
Request:
7065
</span>
71-
<button
72-
onClick={() => copyToClipboard(request.request)}
73-
className="text-blue-500 hover:text-blue-700"
74-
>
75-
<Copy size={16} />
76-
</button>
77-
</div>
78-
<div className="bg-background p-2 rounded">
79-
<JsonView data={request.request} />
8066
</div>
67+
68+
<JsonView
69+
data={request.request}
70+
className="bg-background"
71+
/>
8172
</div>
8273
{request.response && (
8374
<div className="mt-2">
8475
<div className="flex justify-between items-center mb-1">
8576
<span className="font-semibold text-green-600">
8677
Response:
8778
</span>
88-
<button
89-
onClick={() => copyToClipboard(request.response!)}
90-
className="text-blue-500 hover:text-blue-700"
91-
>
92-
<Copy size={16} />
93-
</button>
94-
</div>
95-
<div className="bg-background p-2 rounded">
96-
<JsonView data={request.response} />
9779
</div>
80+
<JsonView
81+
data={request.response}
82+
className="bg-background"
83+
/>
9884
</div>
9985
)}
10086
</>
@@ -134,20 +120,11 @@ const HistoryAndNotifications = ({
134120
<span className="font-semibold text-purple-600">
135121
Details:
136122
</span>
137-
<button
138-
onClick={() =>
139-
copyToClipboard(JSON.stringify(notification))
140-
}
141-
className="text-blue-500 hover:text-blue-700"
142-
>
143-
<Copy size={16} />
144-
</button>
145-
</div>
146-
<div className="bg-background p-2 rounded">
147-
<JsonView
148-
data={JSON.stringify(notification, null, 2)}
149-
/>
150123
</div>
124+
<JsonView
125+
data={JSON.stringify(notification, null, 2)}
126+
className="bg-background"
127+
/>
151128
</div>
152129
)}
153130
</li>

client/src/components/JsonView.tsx

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
import { useState, memo } from "react";
1+
import { useState, memo, useMemo, useCallback, useEffect } from "react";
22
import { JsonValue } from "./DynamicJsonForm";
33
import clsx from "clsx";
4+
import { Copy, CheckCheck } from "lucide-react";
5+
import { Button } from "@/components/ui/button";
6+
import { useToast } from "@/hooks/use-toast";
47

58
interface JsonViewProps {
69
data: unknown;
710
name?: string;
811
initialExpandDepth?: number;
12+
className?: string;
13+
withCopyButton?: boolean;
914
}
1015

1116
function tryParseJson(str: string): { success: boolean; data: JsonValue } {
@@ -24,22 +29,79 @@ function tryParseJson(str: string): { success: boolean; data: JsonValue } {
2429
}
2530

2631
const JsonView = memo(
27-
({ data, name, initialExpandDepth = 3 }: JsonViewProps) => {
28-
const normalizedData =
29-
typeof data === "string"
32+
({
33+
data,
34+
name,
35+
initialExpandDepth = 3,
36+
className,
37+
withCopyButton = true,
38+
}: JsonViewProps) => {
39+
const { toast } = useToast();
40+
const [copied, setCopied] = useState(false);
41+
42+
useEffect(() => {
43+
let timeoutId: NodeJS.Timeout;
44+
if (copied) {
45+
timeoutId = setTimeout(() => {
46+
setCopied(false);
47+
}, 500);
48+
}
49+
return () => {
50+
if (timeoutId) {
51+
clearTimeout(timeoutId);
52+
}
53+
};
54+
}, [copied]);
55+
56+
const normalizedData = useMemo(() => {
57+
return typeof data === "string"
3058
? tryParseJson(data).success
3159
? tryParseJson(data).data
3260
: data
3361
: data;
62+
}, [data]);
63+
64+
const handleCopy = useCallback(() => {
65+
try {
66+
navigator.clipboard.writeText(
67+
typeof normalizedData === "string"
68+
? normalizedData
69+
: JSON.stringify(normalizedData, null, 2),
70+
);
71+
setCopied(true);
72+
} catch (error) {
73+
toast({
74+
title: "Error",
75+
description: `There was an error coping result into the clipboard: ${error instanceof Error ? error.message : String(error)}`,
76+
variant: "destructive",
77+
});
78+
}
79+
}, [toast, normalizedData]);
3480

3581
return (
36-
<div className="font-mono text-sm transition-all duration-300 ">
37-
<JsonNode
38-
data={normalizedData as JsonValue}
39-
name={name}
40-
depth={0}
41-
initialExpandDepth={initialExpandDepth}
42-
/>
82+
<div className={clsx("p-4 border rounded relative", className)}>
83+
{withCopyButton && (
84+
<Button
85+
size="icon"
86+
variant="ghost"
87+
className="absolute top-2 right-2"
88+
onClick={handleCopy}
89+
>
90+
{copied ? (
91+
<CheckCheck className="size-4 dark:text-green-700 text-green-600" />
92+
) : (
93+
<Copy className="size-4 text-foreground" />
94+
)}
95+
</Button>
96+
)}
97+
<div className="font-mono text-sm transition-all duration-300">
98+
<JsonNode
99+
data={normalizedData as JsonValue}
100+
name={name}
101+
depth={0}
102+
initialExpandDepth={initialExpandDepth}
103+
/>
104+
</div>
43105
</div>
44106
);
45107
},

client/src/components/PromptsTab.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,7 @@ const PromptsTab = ({
152152
Get Prompt
153153
</Button>
154154
{promptContent && (
155-
<div className="p-4 border rounded">
156-
<JsonView data={promptContent} />
157-
</div>
155+
<JsonView data={promptContent} withCopyButton={false} />
158156
)}
159157
</div>
160158
) : (

client/src/components/ResourcesTab.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,10 @@ const ResourcesTab = ({
215215
<AlertDescription>{error}</AlertDescription>
216216
</Alert>
217217
) : selectedResource ? (
218-
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded text-sm overflow-auto max-h-96 text-gray-900 dark:text-gray-100">
219-
<JsonView data={resourceContent} />
220-
</div>
218+
<JsonView
219+
data={resourceContent}
220+
className="bg-gray-50 dark:bg-gray-800 p-4 rounded text-sm overflow-auto max-h-96 text-gray-900 dark:text-gray-100"
221+
/>
221222
) : selectedTemplate ? (
222223
<div className="space-y-4">
223224
<p className="text-sm text-gray-600">

client/src/components/SamplingTab.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@ const SamplingTab = ({ pendingRequests, onApprove, onReject }: Props) => {
4444
<h3 className="text-lg font-semibold">Recent Requests</h3>
4545
{pendingRequests.map((request) => (
4646
<div key={request.id} className="p-4 border rounded-lg space-y-4">
47-
<div className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-2 rounded">
48-
<JsonView data={JSON.stringify(request.request)} />
49-
</div>
47+
<JsonView
48+
className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 rounded"
49+
data={JSON.stringify(request.request)}
50+
/>
51+
5052
<div className="flex space-x-2">
5153
<Button onClick={() => handleApprove(request.id)}>Approve</Button>
5254
<Button variant="outline" onClick={() => onReject(request.id)}>

client/src/components/ToolsTab.tsx

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,10 @@ const ToolsTab = ({
5252
return (
5353
<>
5454
<h4 className="font-semibold mb-2">Invalid Tool Result:</h4>
55-
<div className="p-4 border rounded">
56-
<JsonView data={toolResult} />
57-
</div>
55+
<JsonView data={toolResult} />
5856
<h4 className="font-semibold mb-2">Errors:</h4>
5957
{parsedResult.error.errors.map((error, idx) => (
60-
<div key={idx} className="p-4 border rounded">
61-
<JsonView data={error} />
62-
</div>
58+
<JsonView data={error} key={idx} />
6359
))}
6460
</>
6561
);
@@ -74,11 +70,7 @@ const ToolsTab = ({
7470
</h4>
7571
{structuredResult.content.map((item, index) => (
7672
<div key={index} className="mb-2">
77-
{item.type === "text" && (
78-
<div className="p-4 border rounded">
79-
<JsonView data={item.text} />
80-
</div>
81-
)}
73+
{item.type === "text" && <JsonView data={item.text} />}
8274
{item.type === "image" && (
8375
<img
8476
src={`data:${item.mimeType};base64,${item.data}`}
@@ -96,9 +88,7 @@ const ToolsTab = ({
9688
<p>Your browser does not support audio playback</p>
9789
</audio>
9890
) : (
99-
<div className="p-4 border rounded">
100-
<JsonView data={item.resource} />
101-
</div>
91+
<JsonView data={item.resource} />
10292
))}
10393
</div>
10494
))}
@@ -108,9 +98,8 @@ const ToolsTab = ({
10898
return (
10999
<>
110100
<h4 className="font-semibold mb-2">Tool Result (Legacy):</h4>
111-
<div className="p-4 border rounded">
112-
<JsonView data={toolResult.toolResult} />
113-
</div>
101+
102+
<JsonView data={toolResult.toolResult} />
114103
</>
115104
);
116105
}

client/src/components/ui/select.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const SelectTrigger = React.forwardRef<
2222
<SelectPrimitive.Trigger
2323
ref={ref}
2424
className={cn(
25-
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
25+
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 hover:border-[#646cff] hover:border-1",
2626
className,
2727
)}
2828
{...props}

0 commit comments

Comments
 (0)