11import React , { useState , FormEvent , useRef , useEffect } from 'react' ;
22import { Conversation , Message } from '../../types/chat' ;
3- import { Send , Square , Copy , Pencil , Loader2 , Globe , RefreshCw , Check , X } from 'lucide-react' ;
3+ import { MCPServerSettings } from '../../types/settings' ;
4+ import { Send , Square , Copy , Pencil , Loader2 , Globe , RefreshCw , Check , X , ServerCog } from 'lucide-react' ;
45import MarkdownContent from './MarkdownContent' ;
56import MessageToolboxMenu , { ToolboxAction } from '../ui/MessageToolboxMenu' ;
67import { MessageHelper } from '../../services/message-helper' ;
@@ -26,6 +27,9 @@ interface ChatMessageAreaProps {
2627 isCurrentlyStreaming ?: boolean ;
2728 selectedProvider : string ;
2829 selectedModel : string ;
30+ mcpServers ?: Record < string , MCPServerSettings > ;
31+ selectedMcpServers ?: string [ ] ;
32+ onToggleMcpServer ?: ( serverId : string ) => void ;
2933}
3034
3135export const ChatMessageArea : React . FC < ChatMessageAreaProps > = ( {
@@ -40,6 +44,9 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
4044 isCurrentlyStreaming = false ,
4145 selectedProvider,
4246 selectedModel,
47+ mcpServers,
48+ selectedMcpServers,
49+ onToggleMcpServer,
4350} ) => {
4451 const { t } = useTranslation ( ) ;
4552 const [ inputValue , setInput ] = useState ( '' ) ;
@@ -54,6 +61,9 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
5461 const [ webSearchActive , setWebSearchActive ] = useState ( false ) ;
5562 const [ isWebSearchPreviewEnabled , setIsWebSearchPreviewEnabled ] = useState ( false ) ;
5663 const [ selectedFiles , setSelectedFiles ] = useState < File [ ] > ( [ ] ) ;
64+ const [ mcpPopupOpen , setMcpPopupOpen ] = useState ( false ) ;
65+ const mcpButtonRef = useRef < HTMLButtonElement > ( null ) ;
66+ const mcpPopupRef = useRef < HTMLDivElement > ( null ) ;
5767
5868 // Scroll to bottom when messages change
5969 useEffect ( ( ) => {
@@ -278,6 +288,25 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
278288 textarea . style . height = `${ newHeight } px` ;
279289 }
280290
291+ // Add useEffect to handle click outside for MCP popup
292+ useEffect ( ( ) => {
293+ const handleClickOutside = ( event : MouseEvent ) => {
294+ if (
295+ mcpPopupRef . current &&
296+ ! mcpPopupRef . current . contains ( event . target as Node ) &&
297+ mcpButtonRef . current &&
298+ ! mcpButtonRef . current . contains ( event . target as Node )
299+ ) {
300+ setMcpPopupOpen ( false ) ;
301+ }
302+ } ;
303+
304+ document . addEventListener ( 'mousedown' , handleClickOutside ) ;
305+ return ( ) => {
306+ document . removeEventListener ( 'mousedown' , handleClickOutside ) ;
307+ } ;
308+ } , [ ] ) ;
309+
281310 // If no active conversation is selected
282311 if ( ! activeConversation ) {
283312 return (
@@ -552,6 +581,70 @@ export const ChatMessageArea: React.FC<ChatMessageAreaProps> = ({
552581 webSearchElement
553582 }
554583
584+ { /* MCP Servers dropdown */ }
585+ { mcpServers && Object . keys ( mcpServers ) . length > 0 && (
586+ < div className = "relative" >
587+ < button
588+ type = "button"
589+ onClick = { ( ) => setMcpPopupOpen ( ! mcpPopupOpen ) }
590+ ref = { mcpButtonRef }
591+ className = { `flex items-center justify-center w-fit h-8 p-2 transition-all duration-200 rounded-full outline outline-2
592+ ${ selectedMcpServers && selectedMcpServers . length > 0 ? 'bg-blue-50 outline-blue-300 hover:bg-blue-200 hover:outline hover:outline-blue-500' : 'bg-white outline-gray-100 hover:bg-blue-50 hover:outline hover:outline-blue-300' } ` }
593+ aria-label = "MCP Servers"
594+ title = "MCP Servers"
595+ >
596+ < ServerCog className = { `mr-1 ${ selectedMcpServers && selectedMcpServers . length > 0 ? 'text-blue-500' : 'text-gray-400' } transition-all duration-200` } size = { 20 } />
597+ < span className = { `text-sm font-light ${ selectedMcpServers && selectedMcpServers . length > 0 ? 'text-blue-500' : 'text-gray-400' } transition-all duration-200` } >
598+ MCP Tools { selectedMcpServers && selectedMcpServers . length > 0 ? `(${ selectedMcpServers . length } )` : '' }
599+ </ span >
600+ </ button >
601+
602+ { mcpPopupOpen && (
603+ < div
604+ ref = { mcpPopupRef }
605+ className = "absolute z-10 mt-2 image-generation-popup"
606+ style = { { bottom : '100%' , left : 0 , minWidth : '220px' } }
607+ >
608+ < div className = "p-2" >
609+ < div className = "mb-2 text-sm font-medium text-gray-700" >
610+ { t ( 'chat.availableMcpServers' ) }
611+ </ div >
612+ < div className = "overflow-y-auto max-h-60" >
613+ { Object . values ( mcpServers ) . map ( ( server ) => (
614+ < div
615+ key = { server . id }
616+ className = { `flex items-center px-3 py-2 cursor-pointer rounded-md ${
617+ selectedMcpServers ?. includes ( server . id )
618+ ? 'image-generation-provider-selected'
619+ : 'image-generation-provider-item'
620+ } `}
621+ onClick = { ( ) => onToggleMcpServer ?.( server . id ) }
622+ >
623+ < ServerCog className = "w-5 h-5 mr-2" />
624+ < div className = "flex flex-col" >
625+ < span className = "text-sm font-medium" > { server . name } </ span >
626+ { server . isDefault && (
627+ < span className = "text-xs text-gray-500" > { t ( 'mcpServer.default' ) } </ span >
628+ ) }
629+ { server . isImageGeneration && (
630+ < span className = "text-xs text-gray-500" > { t ( 'mcpServer.imageGeneration' ) } </ span >
631+ ) }
632+ </ div >
633+ </ div >
634+ ) ) }
635+
636+ { Object . keys ( mcpServers ) . length === 0 && (
637+ < div className = "px-3 py-2 text-sm text-gray-500" >
638+ { t ( 'chat.noMcpServersAvailable' ) }
639+ </ div >
640+ ) }
641+ </ div >
642+ </ div >
643+ </ div >
644+ ) }
645+ </ div >
646+ ) }
647+
555648 < span className = { `flex-1 hidden text-xs text-center pt-4 text-gray-300 md:block truncate pr-6 lg:pr-12` } >
556649 { t ( 'chat.pressShiftEnterToChangeLines' ) }
557650 </ span >
0 commit comments