Skip to content

Commit 11b7ca5

Browse files
feat: collapsible MCP client config with copy buttons on server detail
1 parent a8e6c34 commit 11b7ca5

1 file changed

Lines changed: 159 additions & 2 deletions

File tree

ui/src/pages/MCPServersPage.tsx

Lines changed: 159 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,15 @@ function IconRefresh() {
114114
)
115115
}
116116

117+
function IconCopy() {
118+
return (
119+
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
120+
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
121+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
122+
</svg>
123+
)
124+
}
125+
117126
// ---------------------------------------------------------------------------
118127
// Helpers
119128
// ---------------------------------------------------------------------------
@@ -545,6 +554,45 @@ function EditMCPServerDialog({ server, onClose }: EditMCPServerDialogProps) {
545554
)
546555
}
547556

557+
// ---------------------------------------------------------------------------
558+
// ClientConfigSnippet
559+
// ---------------------------------------------------------------------------
560+
561+
interface ClientConfigSnippetProps {
562+
label: string | null
563+
config: string
564+
onCopy: () => void
565+
}
566+
567+
function ClientConfigSnippet({ label, config, onCopy }: ClientConfigSnippetProps) {
568+
function handleCopy() {
569+
navigator.clipboard.writeText(config).then(onCopy).catch(() => {
570+
/* clipboard unavailable — fail silently */
571+
})
572+
}
573+
574+
return (
575+
<div className="space-y-1">
576+
{label !== null && (
577+
<span className="text-xs text-text-tertiary">{label}</span>
578+
)}
579+
<div className="relative group">
580+
<pre className="bg-bg-tertiary rounded-md px-3 py-2.5 text-xs font-mono text-text-secondary overflow-x-auto leading-relaxed whitespace-pre">
581+
{config}
582+
</pre>
583+
<button
584+
type="button"
585+
onClick={handleCopy}
586+
className="absolute top-2 right-2 p-1.5 rounded-md text-text-tertiary hover:text-text-primary hover:bg-bg-primary/60 transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100"
587+
aria-label="Copy config to clipboard"
588+
>
589+
<IconCopy />
590+
</button>
591+
</div>
592+
</div>
593+
)
594+
}
595+
548596
// ---------------------------------------------------------------------------
549597
// ServerExpandedRow
550598
// ---------------------------------------------------------------------------
@@ -563,7 +611,54 @@ function cn(...classes: (string | false | undefined | null)[]): string {
563611
return classes.filter(Boolean).join(' ')
564612
}
565613

614+
function getBaseUrl(): string {
615+
const origin = window.location.origin
616+
if (origin.includes('localhost') || origin.includes('127.0.0.1')) {
617+
return 'https://your-voidllm-domain'
618+
}
619+
return origin
620+
}
621+
622+
function generateMCPConfig(alias: string, isCodeMode?: boolean): string {
623+
const base = getBaseUrl()
624+
if (isCodeMode) {
625+
return JSON.stringify(
626+
{
627+
mcpServers: {
628+
'voidllm-code': {
629+
url: `${base}/api/v1/mcp`,
630+
headers: { Authorization: 'Bearer <your-api-key>' },
631+
},
632+
},
633+
},
634+
null,
635+
2,
636+
)
637+
}
638+
return JSON.stringify(
639+
{
640+
mcpServers: {
641+
[alias]: {
642+
url: `${base}/api/v1/mcp/${alias}`,
643+
headers: { Authorization: 'Bearer <your-api-key>' },
644+
},
645+
},
646+
},
647+
null,
648+
2,
649+
)
650+
}
651+
652+
function IconChevronDown() {
653+
return (
654+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
655+
<polyline points="6 9 12 15 18 9" />
656+
</svg>
657+
)
658+
}
659+
566660
function ServerExpandedRow({ server, canModify }: ServerExpandedRowProps) {
661+
const [configOpen, setConfigOpen] = useState(false)
567662
const { data: tools, isLoading: toolsLoading } = useMCPServerTools(server.id)
568663
const addEntry = useAddBlocklistEntry()
569664
const removeEntry = useRemoveBlocklistEntry()
@@ -626,10 +721,71 @@ function ServerExpandedRow({ server, canModify }: ServerExpandedRowProps) {
626721
return addPending || removePending
627722
}
628723

724+
function handleCopyConfig(isCodeMode?: boolean) {
725+
const config = generateMCPConfig(server.alias, isCodeMode)
726+
navigator.clipboard.writeText(config).then(() => {
727+
toast({ variant: 'success', message: 'Copied to clipboard' })
728+
}).catch(() => {})
729+
}
730+
629731
return (
630732
<div className="px-6 py-4 space-y-3">
631-
{/* Header row */}
632-
<div className="flex items-center justify-between">
733+
{/* Client Config — collapsible, above tools */}
734+
<div>
735+
<div className="flex items-center justify-between">
736+
<button
737+
type="button"
738+
onClick={() => setConfigOpen(!configOpen)}
739+
className="inline-flex items-center gap-1.5 text-xs font-medium uppercase tracking-wider text-text-tertiary hover:text-text-secondary transition-colors"
740+
>
741+
<span className={cn('transition-transform', configOpen && 'rotate-180')}>
742+
<IconChevronDown />
743+
</span>
744+
Client Config
745+
</button>
746+
<div className="flex items-center gap-1">
747+
{server.alias === 'voidllm' && (
748+
<button
749+
type="button"
750+
onClick={() => handleCopyConfig(true)}
751+
className="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs text-text-tertiary hover:text-text-primary hover:bg-bg-tertiary transition-colors"
752+
title="Copy Code Mode config"
753+
>
754+
<IconCopy />
755+
Code Mode
756+
</button>
757+
)}
758+
<button
759+
type="button"
760+
onClick={() => handleCopyConfig()}
761+
className="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs text-text-tertiary hover:text-text-primary hover:bg-bg-tertiary transition-colors"
762+
title="Copy config to clipboard"
763+
>
764+
<IconCopy />
765+
Copy
766+
</button>
767+
</div>
768+
</div>
769+
{configOpen && (
770+
<div className="mt-2 space-y-2">
771+
<ClientConfigSnippet
772+
label={null}
773+
config={generateMCPConfig(server.alias)}
774+
onCopy={() => toast({ variant: 'success', message: 'Copied to clipboard' })}
775+
/>
776+
{server.alias === 'voidllm' && (
777+
<ClientConfigSnippet
778+
label="Code Mode (all tools)"
779+
config={generateMCPConfig(server.alias, true)}
780+
onCopy={() => toast({ variant: 'success', message: 'Copied to clipboard' })}
781+
/>
782+
)}
783+
</div>
784+
)}
785+
</div>
786+
787+
{/* Tools header */}
788+
<div className="flex items-center justify-between border-t border-border pt-3">
633789
<span className="text-xs font-medium uppercase tracking-wider text-text-tertiary">
634790
Tools
635791
</span>
@@ -693,6 +849,7 @@ function ServerExpandedRow({ server, canModify }: ServerExpandedRowProps) {
693849
))}
694850
</ul>
695851
)}
852+
696853
</div>
697854
)
698855
}

0 commit comments

Comments
 (0)