Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const router = createTestRouter(() => (
name="test-server"
status="running"
statusContext={undefined}
url=""
/>
))

Expand Down
26 changes: 24 additions & 2 deletions renderer/src/features/mcp-servers/components/card-mcp-server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import {
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
} from '@/common/components/ui/dropdown-menu'
import { Button } from '@/common/components/ui/button'
import { MoreVertical, Trash2, Github, Text } from 'lucide-react'
import { MoreVertical, Trash2, Github, Text, Copy } from 'lucide-react'
import { Link } from '@tanstack/react-router'
import { toast } from 'sonner'
import { Input } from '@/common/components/ui/input'

import type { WorkloadsWorkload } from '@/common/api/generated'
import { ActionsMcpServer } from './actions-mcp-server'
Expand Down Expand Up @@ -76,10 +79,12 @@ export function CardMcpServer({
name,
status,
statusContext,
url,
}: {
name: string
status: WorkloadsWorkload['status']
statusContext: WorkloadsWorkload['status_context']
url: string
}) {
const confirm = useConfirm()
const { mutateAsync: deleteServer, isPending: isDeletePending } =
Expand Down Expand Up @@ -150,6 +155,11 @@ export function CardMcpServer({
}
}, [name, search])

const handleCopyUrl = async () => {
await navigator.clipboard.writeText(url)
toast('MCP server URL has been copied to clipboard')
}

const repositoryUrl = serverDetails?.server?.repository_url

// Check if the server is in deleting state
Expand Down Expand Up @@ -179,7 +189,19 @@ export function CardMcpServer({
<MoreVertical className="h-5 w-5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" role="menu">
<DropdownMenuContent align="end" role="menu" className="w-80">
<div className="flex items-center gap-2 p-2">
<Input value={url} readOnly className="font-mono text-sm" />
<Button
variant="outline"
size="icon"
onClick={handleCopyUrl}
aria-label="Copy URL"
>
<Copy className="size-4" />
</Button>
</div>
<DropdownMenuSeparator />
{repositoryUrl && (
<DropdownMenuItem asChild>
<a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function GridCardsMcpServers({
name={mcpServer.name}
status={mcpServer.status}
statusContext={mcpServer.status_context}
url={mcpServer.url ?? ''}
/>
) : null
)}
Expand Down
39 changes: 38 additions & 1 deletion renderer/src/routes/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,19 @@ Object.defineProperty(window, 'electronAPI', {
writable: true,
})

const mockClipboard = {
writeText: vi.fn(),
readText: vi.fn(),
}

beforeEach(() => {
// Reset mocks before each test
vi.clearAllMocks()

// Mock clipboard API
Object.assign(navigator, {
clipboard: mockClipboard,
})
})

it('renders list of MCP servers', async () => {
Expand Down Expand Up @@ -109,7 +119,7 @@ it('stops server', async () => {
})
})

it('shows dropdown menu with remove option when clicking more options button', async () => {
it('shows dropdown menu with URL and remove option when clicking more options button', async () => {
renderRoute(router)

await waitFor(() => {
Expand All @@ -125,12 +135,39 @@ it('shows dropdown menu with remove option when clicking more options button', a

await userEvent.click(moreOptionsButton)

const urlInput = screen.getByDisplayValue(MOCK_MCP_SERVERS[0].url)
expect(urlInput).toBeVisible()
expect(urlInput).toHaveAttribute('readonly')
expect(screen.getByRole('button', { name: 'Copy URL' })).toBeVisible()

expect(
screen.getByRole('menuitem', { name: /github repository/i })
).toBeVisible()
expect(screen.getByRole('menuitem', { name: /remove/i })).toBeVisible()
})

it('copies URL to clipboard when clicking copy button', async () => {
renderRoute(router)

await waitFor(() => {
expect(screen.getByText('postgres-db')).toBeVisible()
})

const postgresCard = screen
.getByText('postgres-db')
.closest('[data-slot="card"]') as HTMLElement
const moreOptionsButton = within(postgresCard).getByRole('button', {
name: /more options/i,
})

await userEvent.click(moreOptionsButton)

const copyButton = screen.getByRole('button', { name: 'Copy URL' })
await userEvent.click(copyButton)

expect(mockClipboard.writeText).toHaveBeenCalledWith(MOCK_MCP_SERVERS[0].url)
})

it('allows deleting a server through the dropdown menu', async () => {
renderRoute(router)

Expand Down