Skip to content

Commit f4dd419

Browse files
Copilotyaqi-lyu
andauthored
S⚠️ ◾ ✨ Jira preset MCP server with guided domain (#769)
* Initial plan * feat: add Jira preset MCP server with guided domain input Co-authored-by: yaqi-lyu <121055451+yaqi-lyu@users.noreply.github.com> * update mcp domains * refactor connection * remove unncessary changes * fix lint error * fix copilot feedback --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: yaqi-lyu <121055451+yaqi-lyu@users.noreply.github.com> Co-authored-by: Willow Lyu <iqay_lyu@hotmail.com>
1 parent 7b1224a commit f4dd419

File tree

8 files changed

+208
-54
lines changed

8 files changed

+208
-54
lines changed

src/ui/src/components/settings/mcp/McpServerManager.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
} from "../../ui/alert-dialog";
1717
import { McpAzureDevOpsCard } from "./devops/mcp-devops-card";
1818
import { McpGitHubCard } from "./github/mcp-github-card";
19+
import { McpJiraCard } from "./jira/mcp-jira-card";
1920
import type { MCPServerConfig } from "./McpServerForm";
2021
import { McpWhitelistDialog } from "./McpWhitelistDialog";
2122
import { McpCard } from "./mcp-card";
@@ -246,9 +247,13 @@ export function McpSettingsPanel({
246247
const azureDevOps: MCPServerConfig | undefined = sortedServers.find(
247248
(s) => s.id === McpAzureDevOpsCard.Id,
248249
);
250+
const jira: MCPServerConfig | undefined = sortedServers.find((s) => s.id === McpJiraCard.Id);
249251

250252
const restServers: MCPServerConfig[] = sortedServers.filter(
251-
(server) => server.id !== McpGitHubCard.Id && server.id !== McpAzureDevOpsCard.Id,
253+
(server) =>
254+
server.id !== McpGitHubCard.Id &&
255+
server.id !== McpAzureDevOpsCard.Id &&
256+
server.id !== McpJiraCard.Id,
252257
);
253258

254259
function getHealthStatus(serverId?: string | null): HealthStatusInfo | null {
@@ -274,6 +279,13 @@ export function McpSettingsPanel({
274279
onTools={() => azureDevOps && openWhitelistDialog(azureDevOps)}
275280
viewMode={viewMode}
276281
/>
282+
<McpJiraCard
283+
config={jira}
284+
onChange={() => loadServers({ serverIdToRefresh: McpJiraCard.Id })}
285+
healthInfo={getHealthStatus(McpJiraCard.Id)}
286+
onTools={() => jira && openWhitelistDialog(jira)}
287+
viewMode={viewMode}
288+
/>
277289
{restServers.map((server) => (
278290
<>
279291
<McpCard
@@ -304,7 +316,7 @@ export function McpSettingsPanel({
304316
setShowAddCustomMcpForm(true);
305317
}}
306318
>
307-
+ Add custom MCP
319+
+ Add custom MCP server
308320
</div>
309321
</div>
310322
)}

src/ui/src/components/settings/mcp/devops/mcp-devops-card.tsx

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ipcClient } from "@/services/ipc-client";
1+
import { useMcpCardActions } from "@/hooks/useMcpCardActions";
22
import type { HealthStatusInfo } from "../../../../../../backend/types";
33
import type { MCPServerConfig } from "../McpServerForm";
44
import { McpCard } from "../mcp-card";
@@ -36,23 +36,11 @@ export function McpAzureDevOpsCard({
3636
enabled: false,
3737
};
3838

39-
async function toggleSettings(status: boolean): Promise<void> {
40-
const updatedConfig = { ...configLocal, enabled: status };
41-
await ipcClient.mcp.updateServerAsync(McpAzureDevOpsCard.Id, updatedConfig);
42-
43-
if (onChange) {
44-
onChange(updatedConfig);
45-
}
46-
}
47-
48-
function handleOnConnect(): void {
49-
toggleSettings(true);
50-
}
51-
52-
async function handleOnDisconnect(): Promise<void> {
53-
await ipcClient.mcp.clearTokensAsync(McpAzureDevOpsCard.Id);
54-
await toggleSettings(false);
55-
}
39+
const { handleOnConnect, handleOnDisconnect } = useMcpCardActions(
40+
McpAzureDevOpsCard.Id,
41+
configLocal,
42+
onChange,
43+
);
5644

5745
return (
5846
<McpCard

src/ui/src/components/settings/mcp/github/mcp-github-card.tsx

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ipcClient } from "@/services/ipc-client";
1+
import { useMcpCardActions } from "@/hooks/useMcpCardActions";
22
import type { HealthStatusInfo } from "../../../../../../backend/types";
33
import type { MCPServerConfig } from "../McpServerForm";
44
import { McpCard } from "../mcp-card";
@@ -32,23 +32,11 @@ export function McpGitHubCard({
3232
enabled: false,
3333
};
3434

35-
async function toggleSettings(status: boolean): Promise<void> {
36-
const updatedConfig = { ...configLocal, enabled: status };
37-
await ipcClient.mcp.updateServerAsync(McpGitHubCard.Id, updatedConfig);
38-
39-
if (onChange) {
40-
onChange(updatedConfig);
41-
}
42-
}
43-
44-
function handleOnConnect(): void {
45-
toggleSettings(true);
46-
}
47-
48-
async function handleOnDisconnect(): Promise<void> {
49-
await ipcClient.mcp.clearTokensAsync(McpGitHubCard.Id);
50-
await toggleSettings(false);
51-
}
35+
const { handleOnConnect, handleOnDisconnect } = useMcpCardActions(
36+
McpGitHubCard.Id,
37+
configLocal,
38+
onChange,
39+
);
5240

5341
return (
5442
<McpCard
Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type { SVGProps } from "react";
22

33
// Icon from https://svgl.app/
4-
const Atlassian = (props: SVGProps<SVGSVGElement>) => (
4+
export const AtlassianIcon = (props: SVGProps<SVGSVGElement>) => (
55
<svg {...props} preserveAspectRatio="xMidYMid" viewBox="0 0 256 256">
66
<title>Atlassian logo</title>
77
<defs>
88
<linearGradient x1="99.7%" y1="15.8%" x2="39.8%" y2="97.4%" id="atlassian__a">
9-
<stop stop-color="#0052CC" offset="0%" />
10-
<stop stop-color="#2684FF" offset="92.3%" />
9+
<stop stopColor="#0052CC" offset="0%" />
10+
<stop stopColor="#2684FF" offset="92.3%" />
1111
</linearGradient>
1212
</defs>
1313
<path
@@ -20,5 +20,3 @@ const Atlassian = (props: SVGProps<SVGSVGElement>) => (
2020
/>
2121
</svg>
2222
);
23-
24-
export { Atlassian };
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Check, Copy, ExternalLink, X } from "lucide-react";
2+
import { useState } from "react";
3+
import { Button } from "@/components/ui/button";
4+
import { useClipboard } from "@/hooks/useClipboard";
5+
import { useMcpCardActions } from "@/hooks/useMcpCardActions";
6+
import type { HealthStatusInfo } from "@/types";
7+
import type { MCPServerConfig } from "../McpServerForm";
8+
import { McpCard } from "../mcp-card";
9+
import { AtlassianIcon } from "./atlassian";
10+
11+
const YAKSHAVER_DOMAIN = "https://api.yakshaver.ai/**";
12+
const DISMISS_KEY = "jira-setup-dismissed";
13+
14+
interface McpJiraCardProps {
15+
config?: MCPServerConfig;
16+
onChange?: (config: MCPServerConfig) => void;
17+
healthInfo?: HealthStatusInfo | null;
18+
onTools?: () => void;
19+
viewMode: "compact" | "detailed";
20+
}
21+
22+
McpJiraCard.Name = "Jira";
23+
McpJiraCard.Id = "0f03a50c-219b-46e9-9ce3-54f925c44479";
24+
25+
export function McpJiraCard({ config, onChange, healthInfo, onTools, viewMode }: McpJiraCardProps) {
26+
const configLocal = config ?? {
27+
id: McpJiraCard.Id,
28+
name: McpJiraCard.Name,
29+
transport: "streamableHttp",
30+
url: "https://mcp.atlassian.com/v1/mcp",
31+
description: "Atlassian MCP Server",
32+
toolWhitelist: [],
33+
enabled: false,
34+
};
35+
36+
const [dismissed, setDismissed] = useState(() => localStorage.getItem(DISMISS_KEY) === "true");
37+
const { copyToClipboard, copied } = useClipboard();
38+
39+
const { handleOnConnect, handleOnDisconnect } = useMcpCardActions(
40+
McpJiraCard.Id,
41+
configLocal,
42+
onChange,
43+
);
44+
45+
async function handleCopyDomain(): Promise<void> {
46+
await copyToClipboard(YAKSHAVER_DOMAIN, "Domain copied to clipboard");
47+
}
48+
49+
function handleDismiss(): void {
50+
localStorage.setItem(DISMISS_KEY, "true");
51+
setDismissed(true);
52+
}
53+
54+
const prerequisiteSection =
55+
!configLocal.enabled && !dismissed ? (
56+
<div className="mt-4">
57+
<div className="flex flex-col gap-3 rounded-md border border-white/10 bg-white/5 p-3">
58+
<div className="flex items-start justify-between gap-2">
59+
<p className="text-sm font-semibold text-white/90">One-time setup before connecting</p>
60+
<Button
61+
variant="ghost"
62+
size="sm"
63+
className="h-5 w-5 p-0 shrink-0 text-white/40 hover:text-white/70"
64+
onClick={handleDismiss}
65+
title="Dismiss"
66+
>
67+
<X className="size-3.5" />
68+
</Button>
69+
</div>
70+
<p className="text-sm leading-relaxed text-muted-foreground">
71+
Trust YakShaver in your{" "}
72+
<a
73+
href="https://admin.atlassian.com"
74+
target="_blank"
75+
rel="noopener noreferrer"
76+
className="inline-flex items-center gap-0.5 text-blue-400 hover:text-blue-300"
77+
>
78+
Atlassian admin <ExternalLink className="size-3" />
79+
</a>
80+
. Go to{" "}
81+
<span className="text-white/70">
82+
Apps &rsaquo; AI Settings &rsaquo; Rovo MCP server &rsaquo; Your domains
83+
</span>{" "}
84+
and add:
85+
</p>
86+
<div className="flex items-center gap-2 rounded bg-black/20 px-2 py-1.5">
87+
<code className="text-xs text-blue-300 flex-1">{YAKSHAVER_DOMAIN}</code>
88+
<Button
89+
variant="ghost"
90+
size="sm"
91+
className="h-6 px-2 shrink-0"
92+
onClick={handleCopyDomain}
93+
title="Copy domain"
94+
>
95+
{copied ? <Check className="size-3" /> : <Copy className="size-3" />}
96+
</Button>
97+
</div>
98+
</div>
99+
</div>
100+
) : null;
101+
102+
return (
103+
<McpCard
104+
isReadOnly
105+
icon={<AtlassianIcon className="size-8" />}
106+
config={configLocal}
107+
healthInfo={healthInfo}
108+
onConnect={handleOnConnect}
109+
onDisconnect={handleOnDisconnect}
110+
onTools={onTools}
111+
viewMode={viewMode}
112+
extraContent={prerequisiteSection}
113+
/>
114+
);
115+
}

src/ui/src/components/settings/mcp/mcp-card.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface McpCardProps {
1717
onUpdate?: (data: MCPServerConfig) => Promise<void>;
1818
onTools?: () => void;
1919
viewMode: "compact" | "detailed";
20+
extraContent?: React.ReactNode;
2021
}
2122

2223
export function McpCard({
@@ -31,6 +32,7 @@ export function McpCard({
3132
onTools,
3233
healthInfo = null,
3334
viewMode = "compact",
35+
extraContent,
3436
}: McpCardProps) {
3537
const [showSettings, setShowSettings] = useState(false);
3638
return (
@@ -124,6 +126,7 @@ export function McpCard({
124126
</div>
125127
</>
126128
)}
129+
{extraContent}
127130
</div>
128131
</>
129132
);

src/ui/src/hooks/useClipboard.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,31 @@
1+
import { useCallback, useEffect, useRef, useState } from "react";
12
import { toast } from "sonner";
23

3-
export const useClipboard = () => {
4-
const copyToClipboard = async (text: string | null) => {
5-
try {
6-
await navigator.clipboard.writeText(text || "");
7-
toast.success("Copied to clipboard");
8-
} catch {
9-
toast.error("Failed to copy");
10-
}
11-
};
12-
13-
return { copyToClipboard };
4+
export const useClipboard = (duration = 2000) => {
5+
const [copied, setCopied] = useState(false);
6+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
7+
8+
const copyToClipboard = useCallback(
9+
async (text: string | null, message = "Copied to clipboard") => {
10+
try {
11+
await navigator.clipboard.writeText(text ?? "");
12+
setCopied(true);
13+
toast.success(message);
14+
15+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
16+
timeoutRef.current = setTimeout(() => setCopied(false), duration);
17+
} catch {
18+
toast.error("Failed to copy");
19+
}
20+
},
21+
[duration],
22+
);
23+
24+
useEffect(() => {
25+
return () => {
26+
if (timeoutRef.current) clearTimeout(timeoutRef.current);
27+
};
28+
}, []);
29+
30+
return { copied, copyToClipboard };
1431
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { toast } from "sonner";
2+
import type { MCPServerConfig } from "@/components/settings/mcp/McpServerForm";
3+
import { ipcClient } from "@/services/ipc-client";
4+
import { formatErrorMessage } from "@/utils";
5+
6+
export function useMcpCardActions(
7+
serverId: string,
8+
configLocal: MCPServerConfig,
9+
onChange?: (config: MCPServerConfig) => void,
10+
) {
11+
async function toggleSettings(status: boolean): Promise<void> {
12+
const updatedConfig = { ...configLocal, enabled: status };
13+
await ipcClient.mcp.updateServerAsync(serverId, updatedConfig);
14+
if (onChange) {
15+
onChange(updatedConfig);
16+
}
17+
}
18+
19+
async function handleOnConnect(): Promise<void> {
20+
try {
21+
await toggleSettings(true);
22+
} catch (error) {
23+
toast.error(`Failed to connect: ${formatErrorMessage(error)}`);
24+
}
25+
}
26+
27+
async function handleOnDisconnect(): Promise<void> {
28+
await ipcClient.mcp.clearTokensAsync(serverId);
29+
await toggleSettings(false);
30+
}
31+
32+
return { toggleSettings, handleOnConnect, handleOnDisconnect };
33+
}

0 commit comments

Comments
 (0)