@@ -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+
566660function 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