Skip to content

Commit f20fcd8

Browse files
committed
Function: Adding MCP Server
1 parent 92a3025 commit f20fcd8

19 files changed

+1136
-25
lines changed

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ChatPage } from './components/pages/ChatPage';
33
import { ImageGenerationPage } from './components/pages/ImageGenerationPage';
44
import { TranslationPage } from './components/pages/TranslationPage';
55
import { FileManagementPage } from './components/pages/FileManagementPage';
6+
import { MCPServerPage } from './components/pages/MCPServerPage';
67
import MainLayout from './components/layout/MainLayout';
78
import DatabaseInitializer from './components/core/DatabaseInitializer';
89

@@ -38,6 +39,7 @@ function App() {
3839
{activePage === 'image' && <ImageGenerationPage />}
3940
{activePage === 'translation' && <TranslationPage />}
4041
{activePage === 'files' && <FileManagementPage />}
42+
{activePage === 'mcpserver' && <MCPServerPage />}
4143
</MainLayout>
4244
</DatabaseInitializer>
4345
);

src/components/chat/ChatMessageArea.tsx

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useState, FormEvent, useRef, useEffect } from 'react';
22
import { 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';
45
import MarkdownContent from './MarkdownContent';
56
import MessageToolboxMenu, { ToolboxAction } from '../ui/MessageToolboxMenu';
67
import { 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

3135
export 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>

src/components/layout/Sidebar.tsx

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { MessageSquare, Settings, Image, Languages, FolderClosed } from 'lucide-react';
2+
import { MessageSquare, Settings, Image, Languages, FolderClosed, ServerCog } from 'lucide-react';
33

44
interface SidebarProps {
55
activePage: string;
@@ -31,6 +31,9 @@ export const Sidebar: React.FC<SidebarProps> = ({
3131
else if(activePage === 'files'){
3232
return 'files';
3333
}
34+
else if(activePage === 'mcpserver'){
35+
return 'mcpserver';
36+
}
3437

3538
return '';
3639
}
@@ -87,6 +90,18 @@ export const Sidebar: React.FC<SidebarProps> = ({
8790
>
8891
<FolderClosed size={22} />
8992
</button>
93+
94+
<button
95+
className={`w-12 h-12 rounded-lg flex items-center justify-center transition-all duration-200 ${
96+
getActivePage() === 'mcpserver'
97+
? 'navigation-item-selected navigation-item-text'
98+
: 'navigation-item navigation-item-text'
99+
}`}
100+
onClick={() => onChangePage('mcpserver')}
101+
aria-label="MCP Servers"
102+
>
103+
<ServerCog size={22} />
104+
</button>
90105
</div>
91106

92107
{/* Settings button at bottom */}

src/components/pages/ChatPage.tsx

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Conversation, ConversationFolder } from '../../types/chat';
55
import { SETTINGS_CHANGE_EVENT, SettingsService } from '../../services/settings-service';
66
import { ChatService } from '../../services/chat-service';
77
import { AIService } from '../../services/ai-service';
8+
import { MCPServerSettings } from '../../types/settings';
89

910
export const ChatPage = () => {
1011
const [conversations, setConversations] = useState<Conversation[]>([]);
@@ -17,6 +18,8 @@ export const ChatPage = () => {
1718
const [selectedModel, setSelectedModel] = useState('');
1819
const [selectedProvider, setSelectedProvider] = useState('');
1920
const [isApiKeyMissing, setIsApiKeyMissing] = useState(true);
21+
const [mcpServers, setMcpServers] = useState<Record<string, MCPServerSettings>>({});
22+
const [selectedMcpServers, setSelectedMcpServers] = useState<string[]>([]);
2023

2124
// Initialize the services
2225
useEffect(() => {
@@ -43,6 +46,10 @@ export const ChatPage = () => {
4346
const foldersList = chatService.getFolders();
4447
setFolders(foldersList);
4548

49+
// Load MCP servers
50+
const mcpServersList = chatService.getAvailableMCPServers();
51+
setMcpServers(mcpServersList);
52+
4653
// Set active conversation from chat service
4754
const activeId = chatService.getActiveConversationId();
4855
if (activeId) {
@@ -65,6 +72,22 @@ export const ChatPage = () => {
6572

6673
}, [isServiceInitialized]);
6774

75+
// Load MCP servers when settings change
76+
useEffect(() => {
77+
const handleSettingsChange = () => {
78+
if (chatServiceRef.current) {
79+
const mcpServersList = chatServiceRef.current.getAvailableMCPServers();
80+
setMcpServers(mcpServersList);
81+
}
82+
};
83+
84+
window.addEventListener(SETTINGS_CHANGE_EVENT, handleSettingsChange);
85+
86+
return () => {
87+
window.removeEventListener(SETTINGS_CHANGE_EVENT, handleSettingsChange);
88+
};
89+
}, []);
90+
6891
// Load active conversation details when selected
6992
useEffect(() => {
7093
if (activeConversationId && isServiceInitialized && chatServiceRef.current) {
@@ -206,25 +229,32 @@ export const ChatPage = () => {
206229

207230
try {
208231
const chatService = chatServiceRef.current;
209-
210-
// Send user message with streaming
211-
await chatService.sendMessage(
212-
content,
213-
activeConversationId,
214-
true,
215-
(updatedConversation) => {
216-
setConversations(updatedConversation);
217-
}
218-
);
219232

233+
// Check if there are selected MCP servers to use
234+
if (selectedMcpServers.length > 0) {
235+
// Send message with MCP tools
236+
await chatService.sendMessageWithMCPTools(
237+
content,
238+
activeConversationId,
239+
selectedMcpServers,
240+
true,
241+
(updatedConversation) => {
242+
setConversations(updatedConversation);
243+
}
244+
);
245+
} else {
246+
// Send regular message
247+
await chatService.sendMessage(
248+
content,
249+
activeConversationId,
250+
true,
251+
(updatedConversation) => {
252+
setConversations(updatedConversation);
253+
}
254+
);
255+
}
220256
} catch (err) {
221257
console.error('Error sending streaming message:', err);
222-
223-
// // If streaming fails, we'll try to fall back to regular mode
224-
// const error = err as Error;
225-
// if (error.message && error.message.includes('does not support streaming')) {
226-
// await handleSendMessage(content);
227-
// }
228258
}
229259
};
230260

@@ -368,6 +398,17 @@ export const ChatPage = () => {
368398
}
369399
};
370400

401+
// Toggle selection of an MCP server
402+
const handleToggleMcpServer = (serverId: string) => {
403+
setSelectedMcpServers(prev => {
404+
if (prev.includes(serverId)) {
405+
return prev.filter(id => id !== serverId);
406+
} else {
407+
return [...prev, serverId];
408+
}
409+
});
410+
};
411+
371412
return (
372413
<div className="flex flex-col w-full h-full bg-white">
373414

@@ -406,6 +447,9 @@ export const ChatPage = () => {
406447
isCurrentlyStreaming={chatServiceRef.current?.isCurrentlyStreaming(activeConversationId) || false}
407448
selectedProvider={selectedProvider}
408449
selectedModel={selectedModel}
450+
mcpServers={mcpServers}
451+
selectedMcpServers={selectedMcpServers}
452+
onToggleMcpServer={handleToggleMcpServer}
409453
/>
410454
</div>
411455
</div>

0 commit comments

Comments
 (0)