Skip to content

Commit 758d6f7

Browse files
authored
Merge pull request modelcontextprotocol#662 from alex210501/alejandro.borbolla/copy-input
feat: Copy the tool input parameters
2 parents 2352993 + 3075cbb commit 758d6f7

File tree

3 files changed

+92
-40
lines changed

3 files changed

+92
-40
lines changed

client/src/components/JsonView.tsx

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import { useState, memo, useMemo, useCallback, useEffect } from "react";
1+
import { useState, memo, useMemo, useCallback } from "react";
22
import type { JsonValue } from "@/utils/jsonUtils";
33
import clsx from "clsx";
44
import { Copy, CheckCheck } from "lucide-react";
55
import { Button } from "@/components/ui/button";
66
import { useToast } from "@/lib/hooks/useToast";
77
import { getDataType, tryParseJson } from "@/utils/jsonUtils";
8+
import useCopy from "@/lib/hooks/useCopy";
89

910
interface JsonViewProps {
1011
data: unknown;
@@ -25,21 +26,7 @@ const JsonView = memo(
2526
isError = false,
2627
}: JsonViewProps) => {
2728
const { toast } = useToast();
28-
const [copied, setCopied] = useState(false);
29-
30-
useEffect(() => {
31-
let timeoutId: NodeJS.Timeout;
32-
if (copied) {
33-
timeoutId = setTimeout(() => {
34-
setCopied(false);
35-
}, 500);
36-
}
37-
return () => {
38-
if (timeoutId) {
39-
clearTimeout(timeoutId);
40-
}
41-
};
42-
}, [copied]);
29+
const { copied, setCopied } = useCopy();
4330

4431
const normalizedData = useMemo(() => {
4532
return typeof data === "string"

client/src/components/ToolsTab.tsx

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,20 @@ import {
1717
ListToolsResult,
1818
Tool,
1919
} from "@modelcontextprotocol/sdk/types.js";
20-
import { Loader2, Send, ChevronDown, ChevronUp } from "lucide-react";
20+
import {
21+
Loader2,
22+
Send,
23+
ChevronDown,
24+
ChevronUp,
25+
Copy,
26+
CheckCheck,
27+
} from "lucide-react";
2128
import { useEffect, useState } from "react";
2229
import ListPane from "./ListPane";
2330
import JsonView from "./JsonView";
2431
import ToolResults from "./ToolResults";
32+
import { useToast } from "@/lib/hooks/useToast";
33+
import useCopy from "@/lib/hooks/useCopy";
2534

2635
// Type guard to safely detect the optional _meta field without using `any`
2736
const hasMeta = (tool: Tool): tool is Tool & { _meta: unknown } =>
@@ -55,6 +64,8 @@ const ToolsTab = ({
5564
const [isToolRunning, setIsToolRunning] = useState(false);
5665
const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false);
5766
const [isMetaExpanded, setIsMetaExpanded] = useState(false);
67+
const { toast } = useToast();
68+
const { copied, setCopied } = useCopy();
5869

5970
useEffect(() => {
6071
const params = Object.entries(
@@ -291,29 +302,54 @@ const ToolsTab = ({
291302
</div>
292303
</div>
293304
)}
294-
<Button
295-
onClick={async () => {
296-
try {
297-
setIsToolRunning(true);
298-
await callTool(selectedTool.name, params);
299-
} finally {
300-
setIsToolRunning(false);
301-
}
302-
}}
303-
disabled={isToolRunning}
304-
>
305-
{isToolRunning ? (
306-
<>
307-
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
308-
Running...
309-
</>
310-
) : (
311-
<>
312-
<Send className="w-4 h-4 mr-2" />
313-
Run Tool
314-
</>
315-
)}
316-
</Button>
305+
<div className="flex gap-2">
306+
<Button
307+
onClick={async () => {
308+
try {
309+
setIsToolRunning(true);
310+
await callTool(selectedTool.name, params);
311+
} finally {
312+
setIsToolRunning(false);
313+
}
314+
}}
315+
disabled={isToolRunning}
316+
>
317+
{isToolRunning ? (
318+
<>
319+
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
320+
Running...
321+
</>
322+
) : (
323+
<>
324+
<Send className="w-4 h-4 mr-2" />
325+
Run Tool
326+
</>
327+
)}
328+
</Button>
329+
<Button
330+
onClick={async () => {
331+
try {
332+
navigator.clipboard.writeText(
333+
JSON.stringify(params, null, 2),
334+
);
335+
setCopied(true);
336+
} catch (error) {
337+
toast({
338+
title: "Error",
339+
description: `There was an error copying input to the clipboard: ${error instanceof Error ? error.message : String(error)}`,
340+
variant: "destructive",
341+
});
342+
}
343+
}}
344+
>
345+
{copied ? (
346+
<CheckCheck className="h-4 w-4 mr-2 dark:text-green-700 text-green-600" />
347+
) : (
348+
<Copy className="h-4 w-4 mr-2" />
349+
)}
350+
Copy Input
351+
</Button>
352+
</div>
317353
<ToolResults
318354
toolResult={toolResult}
319355
selectedTool={selectedTool}

client/src/lib/hooks/useCopy.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
5+
type UseCopyProps = {
6+
timeout?: number;
7+
};
8+
9+
function useCopy({ timeout = 500 }: UseCopyProps = {}) {
10+
const [copied, setCopied] = useState(false);
11+
12+
useEffect(() => {
13+
let timeoutId: NodeJS.Timeout;
14+
if (copied) {
15+
timeoutId = setTimeout(() => {
16+
setCopied(false);
17+
}, timeout);
18+
}
19+
return () => {
20+
if (timeoutId) {
21+
clearTimeout(timeoutId);
22+
}
23+
};
24+
}, [copied]);
25+
26+
return { copied, setCopied };
27+
}
28+
29+
export default useCopy;

0 commit comments

Comments
 (0)