moelove commited on
Commit
24caae0
·
unverified ·
1 Parent(s): e1fb264

feat: init MCP server (#2)

Browse files

Signed-off-by: Jintao Zhang <zhangjintao9020@gmail.com>

server/index.js CHANGED
@@ -3,6 +3,9 @@ import cors from 'cors';
3
  import fetch from 'node-fetch';
4
  import path from 'path'
5
  import { fileURLToPath } from 'url'
 
 
 
6
 
7
  const app = express();
8
  const port = 7860;
@@ -15,7 +18,7 @@ app.use(express.json());
15
 
16
  app.post('/api/summarize', async (req, res) => {
17
  const { content, apiEndpoint, apiKey, model } = req.body;
18
-
19
  console.log('Received summarize request with:', {
20
  apiEndpoint,
21
  model,
@@ -32,7 +35,7 @@ app.post('/api/summarize', async (req, res) => {
32
  apiUrl = `${apiEndpoint}/v1/chat/completions`;
33
  }
34
  console.log('Calling API endpoint:', apiUrl);
35
-
36
  const response = await fetch(apiUrl, {
37
  method: 'POST',
38
  headers: {
@@ -70,7 +73,7 @@ app.post('/api/summarize', async (req, res) => {
70
 
71
  app.post('/api/chat', async (req, res) => {
72
  const { messages, apiEndpoint, apiKey, model } = req.body;
73
-
74
  console.log('Received chat request with:', {
75
  apiEndpoint,
76
  model,
@@ -87,7 +90,7 @@ app.post('/api/chat', async (req, res) => {
87
  apiUrl = `${apiEndpoint}/v1/chat/completions`;
88
  }
89
  console.log('Calling API endpoint:', apiUrl);
90
-
91
  const response = await fetch(apiUrl, {
92
  method: 'POST',
93
  headers: {
@@ -148,6 +151,51 @@ if (process.env.NODE_ENV === 'production') {
148
  })
149
  }
150
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  app.listen(port, () => {
152
  console.log(`Server running at http://localhost:${port}`);
153
  });
 
3
  import fetch from 'node-fetch';
4
  import path from 'path'
5
  import { fileURLToPath } from 'url'
6
+ import { callMcpServer, discoverMcpServerTools, executeMcpTool } from './mcp.js';
7
+
8
+ // Note: Run the mock MCP server separately with: node server/start-mock-mcp.js
9
 
10
  const app = express();
11
  const port = 7860;
 
18
 
19
  app.post('/api/summarize', async (req, res) => {
20
  const { content, apiEndpoint, apiKey, model } = req.body;
21
+
22
  console.log('Received summarize request with:', {
23
  apiEndpoint,
24
  model,
 
35
  apiUrl = `${apiEndpoint}/v1/chat/completions`;
36
  }
37
  console.log('Calling API endpoint:', apiUrl);
38
+
39
  const response = await fetch(apiUrl, {
40
  method: 'POST',
41
  headers: {
 
73
 
74
  app.post('/api/chat', async (req, res) => {
75
  const { messages, apiEndpoint, apiKey, model } = req.body;
76
+
77
  console.log('Received chat request with:', {
78
  apiEndpoint,
79
  model,
 
90
  apiUrl = `${apiEndpoint}/v1/chat/completions`;
91
  }
92
  console.log('Calling API endpoint:', apiUrl);
93
+
94
  const response = await fetch(apiUrl, {
95
  method: 'POST',
96
  headers: {
 
151
  })
152
  }
153
 
154
+ // MCP endpoints
155
+ app.post('/api/mcp/discover', async (req, res) => {
156
+ const { server } = req.body;
157
+
158
+ console.log('Received MCP discovery request for server:', server);
159
+
160
+ if (!server || !server.endpoint) {
161
+ console.log('Invalid server configuration');
162
+ return res.status(400).json({ error: 'Invalid server configuration' });
163
+ }
164
+
165
+ try {
166
+ console.log(`Discovering tools from MCP server: ${server.name} at ${server.endpoint}`);
167
+ const toolsInfo = await discoverMcpServerTools(server);
168
+ console.log('Discovered tools:', toolsInfo);
169
+ res.json(toolsInfo);
170
+ } catch (error) {
171
+ console.error('Error discovering MCP tools:', error);
172
+ res.status(500).json({ error: error.message });
173
+ }
174
+ });
175
+
176
+ app.post('/api/mcp/execute', async (req, res) => {
177
+ const { server, tool, parameters } = req.body;
178
+
179
+ console.log(`Received MCP tool execution request: ${tool}`);
180
+ console.log('Server:', server);
181
+ console.log('Parameters:', parameters);
182
+
183
+ if (!server || !server.endpoint || !tool) {
184
+ console.log('Invalid request parameters');
185
+ return res.status(400).json({ error: 'Invalid request parameters' });
186
+ }
187
+
188
+ try {
189
+ console.log(`Executing MCP tool ${tool} on server ${server.name}`);
190
+ const result = await executeMcpTool(server, tool, parameters);
191
+ console.log('Tool execution result:', result);
192
+ res.json(result);
193
+ } catch (error) {
194
+ console.error('Error executing MCP tool:', error);
195
+ res.status(500).json({ error: error.message });
196
+ }
197
+ });
198
+
199
  app.listen(port, () => {
200
  console.log(`Server running at http://localhost:${port}`);
201
  });
server/mcp.js ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // MCP (Model Context Protocol) client implementation
2
+ import fetch from 'node-fetch';
3
+
4
+ // Function to call an MCP server
5
+ export async function callMcpServer(server, request) {
6
+ try {
7
+ console.log(`Calling MCP server: ${server.name} at ${server.endpoint}`);
8
+
9
+ const headers = {
10
+ 'Content-Type': 'application/json'
11
+ };
12
+
13
+ // Add authentication if provided
14
+ if (server.authToken) {
15
+ headers['Authorization'] = `Bearer ${server.authToken}`;
16
+ }
17
+
18
+ const response = await fetch(server.endpoint, {
19
+ method: 'POST',
20
+ headers,
21
+ body: JSON.stringify(request)
22
+ });
23
+
24
+ if (!response.ok) {
25
+ const errorText = await response.text();
26
+ console.error(`MCP server error: ${response.status}`, errorText);
27
+ throw new Error(`MCP server error: ${response.status} - ${errorText}`);
28
+ }
29
+
30
+ return await response.json();
31
+ } catch (error) {
32
+ console.error(`Error calling MCP server ${server.name}:`, error);
33
+ throw error;
34
+ }
35
+ }
36
+
37
+ // Function to discover available tools from an MCP server
38
+ export async function discoverMcpServerTools(server) {
39
+ try {
40
+ console.log(`[MCP] Discovering tools from server ${server.name} at ${server.endpoint}`);
41
+ const discoveryRequest = {
42
+ type: 'discovery'
43
+ };
44
+
45
+ console.log('[MCP] Sending discovery request:', discoveryRequest);
46
+ const response = await callMcpServer(server, discoveryRequest);
47
+ console.log('[MCP] Discovery response:', response);
48
+
49
+ const result = {
50
+ serverId: server.id,
51
+ serverName: server.name,
52
+ tools: response.tools || []
53
+ };
54
+ console.log('[MCP] Processed discovery result:', result);
55
+ return result;
56
+ } catch (error) {
57
+ console.error(`[MCP] Error discovering tools from MCP server ${server.name}:`, error);
58
+ return {
59
+ serverId: server.id,
60
+ serverName: server.name,
61
+ tools: [],
62
+ error: error.message
63
+ };
64
+ }
65
+ }
66
+
67
+ // Function to execute a tool on an MCP server
68
+ export async function executeMcpTool(server, toolName, parameters) {
69
+ try {
70
+ const toolRequest = {
71
+ type: 'tool_execution',
72
+ tool: toolName,
73
+ parameters
74
+ };
75
+
76
+ return await callMcpServer(server, toolRequest);
77
+ } catch (error) {
78
+ console.error(`Error executing tool ${toolName} on MCP server ${server.name}:`, error);
79
+ throw error;
80
+ }
81
+ }
server/mock-mcp-server.js ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Mock MCP server for testing
2
+ import express from 'express';
3
+ import cors from 'cors';
4
+
5
+ const app = express();
6
+ const port = 7861;
7
+
8
+ app.use(cors());
9
+ app.use(express.json());
10
+
11
+ // Mock MCP tools
12
+ const mockTools = [
13
+ {
14
+ name: 'web-search',
15
+ description: 'Search the web for information',
16
+ parameters: {
17
+ query: {
18
+ type: 'string',
19
+ description: 'The search query'
20
+ }
21
+ }
22
+ },
23
+ {
24
+ name: 'weather',
25
+ description: 'Get current weather information',
26
+ parameters: {
27
+ location: {
28
+ type: 'string',
29
+ description: 'The location to get weather for'
30
+ }
31
+ }
32
+ },
33
+ {
34
+ name: 'calculator',
35
+ description: 'Perform mathematical calculations',
36
+ parameters: {
37
+ expression: {
38
+ type: 'string',
39
+ description: 'The mathematical expression to evaluate'
40
+ }
41
+ }
42
+ }
43
+ ];
44
+
45
+ // MCP discovery endpoint
46
+ app.post('/', (req, res) => {
47
+ const { type } = req.body;
48
+
49
+ if (type === 'discovery') {
50
+ console.log('Received discovery request');
51
+ res.json({
52
+ tools: mockTools
53
+ });
54
+ } else if (type === 'tool_execution') {
55
+ const { tool, parameters } = req.body;
56
+ console.log(`Executing tool: ${tool} with parameters:`, parameters);
57
+
58
+ // Mock responses for different tools
59
+ if (tool === 'web-search') {
60
+ res.json({
61
+ result: `Search results for: ${parameters.query || 'unknown query'}`,
62
+ links: [
63
+ { title: 'Example result 1', url: 'https://example.com/1' },
64
+ { title: 'Example result 2', url: 'https://example.com/2' }
65
+ ]
66
+ });
67
+ } else if (tool === 'weather') {
68
+ res.json({
69
+ location: parameters.location || 'unknown location',
70
+ temperature: '22°C',
71
+ condition: 'Sunny',
72
+ humidity: '45%'
73
+ });
74
+ } else if (tool === 'calculator') {
75
+ let result;
76
+ try {
77
+ // Simple evaluation (not secure for production)
78
+ result = eval(parameters.expression);
79
+ } catch (error) {
80
+ result = 'Error evaluating expression';
81
+ }
82
+
83
+ res.json({
84
+ expression: parameters.expression,
85
+ result: result
86
+ });
87
+ } else {
88
+ res.status(400).json({ error: `Unknown tool: ${tool}` });
89
+ }
90
+ } else {
91
+ res.status(400).json({ error: 'Invalid request type' });
92
+ }
93
+ });
94
+
95
+ const server = app.listen(port, () => {
96
+ console.log(`Mock MCP server running at http://localhost:${port}`);
97
+ });
98
+
99
+ export default { app, server };
server/start-mock-mcp.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ // Start the mock MCP server
2
+ import './mock-mcp-server.js';
src/App.jsx CHANGED
@@ -23,6 +23,8 @@ function App() {
23
  model: 'DeepSeek-R1'
24
  });
25
 
 
 
26
  const [activeProfileId, setActiveProfileId] = useLocalStorage('activeProfileId', 'default');
27
 
28
  const [chats, setChats] = useLocalStorage('chats', []);
@@ -228,6 +230,7 @@ function App() {
228
  isStreamingChat={isStreamingChat}
229
  allChats={chats}
230
  currentChatId={currentChatId}
 
231
  />
232
  ) : (
233
  <div className="flex flex-col items-center justify-center h-full p-5 text-center">
@@ -262,6 +265,8 @@ function App() {
262
  }}
263
  onSaveSummarizationProfile={setSummarizationProfile}
264
  summarizationProfile={summarizationProfile}
 
 
265
  onChangeActiveProfile={setActiveProfileId}
266
  onCloseSettings={() => setShowSettings(false)}
267
  />
 
23
  model: 'DeepSeek-R1'
24
  });
25
 
26
+ const [mcpServers, setMcpServers] = useLocalStorage('mcpServers', []);
27
+
28
  const [activeProfileId, setActiveProfileId] = useLocalStorage('activeProfileId', 'default');
29
 
30
  const [chats, setChats] = useLocalStorage('chats', []);
 
230
  isStreamingChat={isStreamingChat}
231
  allChats={chats}
232
  currentChatId={currentChatId}
233
+ mcpServers={mcpServers}
234
  />
235
  ) : (
236
  <div className="flex flex-col items-center justify-center h-full p-5 text-center">
 
265
  }}
266
  onSaveSummarizationProfile={setSummarizationProfile}
267
  summarizationProfile={summarizationProfile}
268
+ mcpServers={mcpServers}
269
+ onSaveMcpServers={setMcpServers}
270
  onChangeActiveProfile={setActiveProfileId}
271
  onCloseSettings={() => setShowSettings(false)}
272
  />
src/components/ChatWindow.jsx CHANGED
@@ -1,5 +1,6 @@
1
  import React, { useState, useRef, useEffect } from 'react';
2
  import ReactMarkdown from 'react-markdown';
 
3
 
4
  function ChatWindow({
5
  chat,
@@ -11,7 +12,8 @@ function ChatWindow({
11
  removeStreamingChat,
12
  isStreamingChat,
13
  allChats,
14
- currentChatId
 
15
  }) {
16
  const [input, setInput] = useState('');
17
  const [isLoading, setIsLoading] = useState(false);
@@ -19,8 +21,55 @@ function ChatWindow({
19
  const [partialResponse, setPartialResponse] = useState('');
20
  const [streamController, setStreamController] = useState(null);
21
  const [copiedMessageId, setCopiedMessageId] = useState(null);
 
22
  const messagesEndRef = useRef(null);
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  // Scroll to bottom when messages change
25
  useEffect(() => {
26
  if (chat && chat.messages) {
@@ -53,6 +102,184 @@ function ChatWindow({
53
  setCollapsedThinks(newCollapsed);
54
  };
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  const handleSendMessage = async () => {
57
  if (!input.trim()) return;
58
  if (!chat || !chat.messages) return; // Add safety check
@@ -62,6 +289,145 @@ function ChatWindow({
62
  const userInput = input; // 保存用户输入,以便在流结束后仍能访问
63
  console.log(`Starting message send for chat ${currentChatID}`);
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  // Cancel any ongoing stream for this chat
66
  if (streamController) {
67
  streamController.abort();
@@ -341,6 +707,9 @@ function ChatWindow({
341
  });
342
  };
343
 
 
 
 
344
  return (
345
  <div className="chat-window relative">
346
  {/* Floating New Chat button */}
@@ -440,6 +809,16 @@ function ChatWindow({
440
 
441
  <div className="chat-input">
442
  <div className="flex items-center gap-2 w-full">
 
 
 
 
 
 
 
 
 
 
443
  <textarea
444
  className="message-input"
445
  value={input}
@@ -460,9 +839,45 @@ function ChatWindow({
460
  >
461
  Send
462
  </button>
463
-
464
  </div>
465
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  </div>
467
  </div>
468
  );
 
1
  import React, { useState, useRef, useEffect } from 'react';
2
  import ReactMarkdown from 'react-markdown';
3
+ import useMcp from '../hooks/useMcp';
4
 
5
  function ChatWindow({
6
  chat,
 
12
  removeStreamingChat,
13
  isStreamingChat,
14
  allChats,
15
+ currentChatId,
16
+ mcpServers
17
  }) {
18
  const [input, setInput] = useState('');
19
  const [isLoading, setIsLoading] = useState(false);
 
21
  const [partialResponse, setPartialResponse] = useState('');
22
  const [streamController, setStreamController] = useState(null);
23
  const [copiedMessageId, setCopiedMessageId] = useState(null);
24
+ const [showMcpTools, setShowMcpTools] = useState(false);
25
  const messagesEndRef = useRef(null);
26
 
27
+ // Initialize MCP hook
28
+ const { getAllTools, executeTool } = useMcp(mcpServers);
29
+
30
+ // 检测是否需要使用MCP工具来回答用户的问题
31
+ const shouldUseMcpTool = (userInput) => {
32
+ // 如果没有可用的MCP工具,则不使用
33
+ const availableTools = getAllTools();
34
+ if (!availableTools || availableTools.length === 0) {
35
+ return false;
36
+ }
37
+
38
+ // 定义可能需要使用MCP工具的问题类型
39
+ const informationQuestions = [
40
+ 'what is', 'who is', 'tell me about', 'how to', 'where is',
41
+ 'when was', 'why is', 'how does', 'what are', 'can you explain',
42
+ 'information about', 'details on', 'facts about', 'history of',
43
+ 'latest news', 'current events', 'recent developments'
44
+ ];
45
+
46
+ const weatherQuestions = [
47
+ 'weather', 'temperature', 'forecast', 'how hot', 'how cold',
48
+ 'is it raining', 'is it sunny', 'will it rain', 'climate'
49
+ ];
50
+
51
+ const calculationQuestions = [
52
+ 'calculate', 'compute', 'solve', 'what is', 'evaluate',
53
+ 'plus', 'minus', 'times', 'divided by', 'square root',
54
+ 'percentage', 'factorial', 'exponent', 'logarithm'
55
+ ];
56
+
57
+ // 检查用户输入是否包含这些问题类型的关键词
58
+ const input = userInput.toLowerCase();
59
+
60
+ // 检查是否是信息查询类型的问题
61
+ const isInformationQuestion = informationQuestions.some(keyword => input.includes(keyword.toLowerCase()));
62
+
63
+ // 检查是否是天气查询类型的问题
64
+ const isWeatherQuestion = weatherQuestions.some(keyword => input.includes(keyword.toLowerCase()));
65
+
66
+ // 检查是否是计算类型的问题
67
+ const isCalculationQuestion = calculationQuestions.some(keyword => input.includes(keyword.toLowerCase()));
68
+
69
+ // 如果是任何一种类型的问题,则可能需要使用MCP工具
70
+ return isInformationQuestion || isWeatherQuestion || isCalculationQuestion;
71
+ };
72
+
73
  // Scroll to bottom when messages change
74
  useEffect(() => {
75
  if (chat && chat.messages) {
 
102
  setCollapsedThinks(newCollapsed);
103
  };
104
 
105
+ // Handle MCP tool execution
106
+ const handleExecuteMcpTool = async (serverId, toolName, parameters) => {
107
+ try {
108
+ setIsLoading(true);
109
+ const result = await executeTool(serverId, toolName, parameters);
110
+
111
+ // Add the tool execution and result to the chat
112
+ const toolMessage = {
113
+ role: 'user',
114
+ content: `Executing MCP tool: ${toolName}`,
115
+ timestamp: Date.now(),
116
+ mcpTool: {
117
+ serverId,
118
+ toolName,
119
+ parameters
120
+ }
121
+ };
122
+
123
+ const resultMessage = {
124
+ role: 'assistant',
125
+ content: JSON.stringify(result, null, 2),
126
+ timestamp: Date.now(),
127
+ mcpResult: true
128
+ };
129
+
130
+ const updatedChat = {
131
+ ...chat,
132
+ messages: [...chat.messages, toolMessage, resultMessage]
133
+ };
134
+
135
+ onUpdateChat(updatedChat);
136
+ setShowMcpTools(false);
137
+ } catch (error) {
138
+ console.error('Error executing MCP tool:', error);
139
+
140
+ // Add error message to chat
141
+ const errorMessage = {
142
+ role: 'assistant',
143
+ content: `Error executing MCP tool: ${error.message}`,
144
+ timestamp: Date.now(),
145
+ error: true
146
+ };
147
+
148
+ const updatedChat = {
149
+ ...chat,
150
+ messages: [...chat.messages, errorMessage]
151
+ };
152
+
153
+ onUpdateChat(updatedChat);
154
+ } finally {
155
+ setIsLoading(false);
156
+ }
157
+ };
158
+
159
+ // 检测用户输入是否需要使用MCP工具
160
+ const detectMcpToolRequest = (userInput) => {
161
+ console.log('Detecting MCP tool request for:', userInput);
162
+ // 获取所有可用的MCP工具
163
+ const availableTools = getAllTools();
164
+ if (!availableTools || availableTools.length === 0) {
165
+ return null;
166
+ }
167
+
168
+ // 定义工具的意图模式和关键词
169
+ const toolPatterns = {
170
+ 'web-search': {
171
+ intents: ['search', 'find', 'look up', 'google', 'information about', 'tell me about', 'what is', 'who is'],
172
+ paramExtractor: (input) => {
173
+ // 尝试不同的模式来提取查询
174
+ const patterns = [
175
+ /search\s+for\s+([\w\s\d\-\.,?!]+)/i,
176
+ /search\s+([\w\s\d\-\.,?!]+)/i,
177
+ /find\s+([\w\s\d\-\.,?!]+)/i,
178
+ /look\s+up\s+([\w\s\d\-\.,?!]+)/i,
179
+ /information\s+about\s+([\w\s\d\-\.,?!]+)/i,
180
+ /tell\s+me\s+about\s+([\w\s\d\-\.,?!]+)/i,
181
+ /what\s+is\s+([\w\s\d\-\.,?!]+)/i,
182
+ /who\s+is\s+([\w\s\d\-\.,?!]+)/i
183
+ ];
184
+
185
+ for (const pattern of patterns) {
186
+ const match = input.match(pattern);
187
+ if (match && match[1]) {
188
+ return { query: match[1].trim() };
189
+ }
190
+ }
191
+
192
+ // 如果没有匹配到特定模式,使用整个输入作为查询
193
+ return { query: input.trim() };
194
+ }
195
+ },
196
+ 'weather': {
197
+ intents: ['weather', 'temperature', 'forecast', 'how hot', 'how cold', 'raining', 'sunny'],
198
+ paramExtractor: (input) => {
199
+ // 尝试不同的模式来提取位置
200
+ const patterns = [
201
+ /weather\s+in\s+([\w\s\d\-\.,]+)/i,
202
+ /temperature\s+in\s+([\w\s\d\-\.,]+)/i,
203
+ /forecast\s+for\s+([\w\s\d\-\.,]+)/i,
204
+ /how\s+(?:hot|cold)\s+is\s+it\s+in\s+([\w\s\d\-\.,]+)/i,
205
+ /is\s+it\s+(?:raining|sunny)\s+in\s+([\w\s\d\-\.,]+)/i
206
+ ];
207
+
208
+ for (const pattern of patterns) {
209
+ const match = input.match(pattern);
210
+ if (match && match[1]) {
211
+ return { location: match[1].trim() };
212
+ }
213
+ }
214
+
215
+ // 如果没有指定位置,使用默认位置
216
+ return { location: 'current location' };
217
+ }
218
+ },
219
+ 'calculator': {
220
+ intents: ['calculate', 'compute', 'math', 'solve', 'what is', 'evaluate'],
221
+ paramExtractor: (input) => {
222
+ // 尝试不同的模式来提取表达式
223
+ const patterns = [
224
+ /calculate\s+([\d\+\-\*\/\(\)\s\.]+)/i,
225
+ /compute\s+([\d\+\-\*\/\(\)\s\.]+)/i,
226
+ /solve\s+([\d\+\-\*\/\(\)\s\.]+)/i,
227
+ /what\s+is\s+([\d\+\-\*\/\(\)\s\.]+)/i,
228
+ /evaluate\s+([\d\+\-\*\/\(\)\s\.]+)/i
229
+ ];
230
+
231
+ for (const pattern of patterns) {
232
+ const match = input.match(pattern);
233
+ if (match && match[1]) {
234
+ return { expression: match[1].trim() };
235
+ }
236
+ }
237
+
238
+ // 如果没有匹配到特定模式,尝试提取数学表达式
239
+ const mathExpressionMatch = input.match(/([\d\+\-\*\/\(\)\s\.]+)/i);
240
+ if (mathExpressionMatch && mathExpressionMatch[1]) {
241
+ return { expression: mathExpressionMatch[1].trim() };
242
+ }
243
+
244
+ return { expression: '' };
245
+ }
246
+ }
247
+ };
248
+
249
+ // 检查每个工具
250
+ for (const tool of availableTools) {
251
+ const toolPattern = toolPatterns[tool.name];
252
+
253
+ // 如果有这个工具的模式定义
254
+ if (toolPattern) {
255
+ // 检查是否匹配任何意图
256
+ const matchesIntent = toolPattern.intents.some(intent =>
257
+ userInput.toLowerCase().includes(intent.toLowerCase())
258
+ );
259
+
260
+ if (matchesIntent) {
261
+ // 提取参数
262
+ const parameters = toolPattern.paramExtractor(userInput);
263
+
264
+ return {
265
+ tool,
266
+ parameters
267
+ };
268
+ }
269
+ } else {
270
+ // 如果没有定义特定模式,使用简单的关键词匹配
271
+ if (userInput.toLowerCase().includes(tool.name.toLowerCase())) {
272
+ return {
273
+ tool,
274
+ parameters: {}
275
+ };
276
+ }
277
+ }
278
+ }
279
+
280
+ return null;
281
+ };
282
+
283
  const handleSendMessage = async () => {
284
  if (!input.trim()) return;
285
  if (!chat || !chat.messages) return; // Add safety check
 
289
  const userInput = input; // 保存用户输入,以便在流结束后仍能访问
290
  console.log(`Starting message send for chat ${currentChatID}`);
291
 
292
+ // 检测是否需要使用MCP工具
293
+ let toolRequest = detectMcpToolRequest(userInput);
294
+
295
+ // 如果没有直接检测到工具请求,但问题类型可能需要使用MCP工具
296
+ if (!toolRequest && shouldUseMcpTool(userInput)) {
297
+ console.log('Question might benefit from MCP tools, trying to find a suitable tool...');
298
+
299
+ // 获取所有可用的工具
300
+ const availableTools = getAllTools();
301
+
302
+ // 尝试找到最适合的工具
303
+ if (userInput.toLowerCase().includes('weather') ||
304
+ userInput.toLowerCase().includes('temperature') ||
305
+ userInput.toLowerCase().includes('forecast')) {
306
+ // 天气相关问题
307
+ const weatherTool = availableTools.find(tool => tool.name === 'weather');
308
+ if (weatherTool) {
309
+ // 提取位置
310
+ const locationMatch = userInput.match(/(?:in|at|for)\s+([\w\s,]+)(?:\?|\.|$)/i);
311
+ const location = locationMatch ? locationMatch[1].trim() : 'current location';
312
+
313
+ toolRequest = {
314
+ tool: weatherTool,
315
+ parameters: { location }
316
+ };
317
+ }
318
+ } else if (userInput.match(/[\d\+\-\*\/\(\)]/)) {
319
+ // 包含数学表达式
320
+ const calculatorTool = availableTools.find(tool => tool.name === 'calculator');
321
+ if (calculatorTool) {
322
+ const expressionMatch = userInput.match(/([\d\+\-\*\/\(\)\s\.]+)/i);
323
+ const expression = expressionMatch ? expressionMatch[1].trim() : '';
324
+
325
+ toolRequest = {
326
+ tool: calculatorTool,
327
+ parameters: { expression }
328
+ };
329
+ }
330
+ } else {
331
+ // 其他信息查询问题
332
+ const searchTool = availableTools.find(tool => tool.name === 'web-search');
333
+ if (searchTool) {
334
+ toolRequest = {
335
+ tool: searchTool,
336
+ parameters: { query: userInput }
337
+ };
338
+ }
339
+ }
340
+ }
341
+
342
+ // 如果检测到工具请求,自动使用相应的工具
343
+ if (toolRequest) {
344
+ console.log(`Detected MCP tool request: ${toolRequest.tool.name}`);
345
+ console.log(`Parameters: `, toolRequest.parameters);
346
+
347
+ // 添加用户消息
348
+ const newMessage = {
349
+ role: 'user',
350
+ content: userInput,
351
+ timestamp: Date.now()
352
+ };
353
+
354
+ const updatedChat = {
355
+ ...chat,
356
+ messages: [...chat.messages, newMessage]
357
+ };
358
+ onUpdateChat(updatedChat);
359
+ setInput('');
360
+
361
+ // 执行工具
362
+ try {
363
+ setIsLoading(true);
364
+ const result = await executeTool(toolRequest.tool.serverId, toolRequest.tool.name, toolRequest.parameters);
365
+
366
+ // 添加工具执行结果消息
367
+ let formattedResult = '';
368
+
369
+ // 根据工具类型格式化结果
370
+ if (toolRequest.tool.name === 'web-search') {
371
+ formattedResult = `**Search Results:**\n\n`;
372
+ if (result.links && Array.isArray(result.links)) {
373
+ result.links.forEach((link, index) => {
374
+ formattedResult += `${index + 1}. [${link.title}](${link.url})\n`;
375
+ });
376
+ } else {
377
+ formattedResult += result.result || JSON.stringify(result, null, 2);
378
+ }
379
+ } else if (toolRequest.tool.name === 'weather') {
380
+ formattedResult = `**Weather in ${result.location || 'the requested location'}:**\n\n`;
381
+ formattedResult += `- Temperature: ${result.temperature || 'N/A'}\n`;
382
+ formattedResult += `- Condition: ${result.condition || 'N/A'}\n`;
383
+ formattedResult += `- Humidity: ${result.humidity || 'N/A'}\n`;
384
+ } else if (toolRequest.tool.name === 'calculator') {
385
+ formattedResult = `**Calculation Result:**\n\n`;
386
+ formattedResult += `Expression: ${result.expression || toolRequest.parameters.expression}\n`;
387
+ formattedResult += `Result: ${result.result !== undefined ? result.result : 'Error in calculation'}\n`;
388
+ } else {
389
+ // 其他工具类型的默认格式
390
+ formattedResult = JSON.stringify(result, null, 2);
391
+ }
392
+
393
+ const toolMessage = {
394
+ role: 'assistant',
395
+ content: `I used the ${toolRequest.tool.name} tool to help answer your question.\n\n${formattedResult}`,
396
+ timestamp: Date.now(),
397
+ mcpResult: true
398
+ };
399
+
400
+ const finalChat = {
401
+ ...updatedChat,
402
+ messages: [...updatedChat.messages, toolMessage]
403
+ };
404
+
405
+ onUpdateChat(finalChat);
406
+ } catch (error) {
407
+ console.error('Error executing MCP tool:', error);
408
+
409
+ // 添加错误消息
410
+ const errorMessage = {
411
+ role: 'assistant',
412
+ content: `I tried to use the ${toolRequest.tool.name} tool, but encountered an error: ${error.message}`,
413
+ timestamp: Date.now(),
414
+ error: true
415
+ };
416
+
417
+ const errorChat = {
418
+ ...updatedChat,
419
+ messages: [...updatedChat.messages, errorMessage]
420
+ };
421
+
422
+ onUpdateChat(errorChat);
423
+ } finally {
424
+ setIsLoading(false);
425
+ }
426
+
427
+ return;
428
+ }
429
+
430
+ // 如果不是工具请求,正常发送消息
431
  // Cancel any ongoing stream for this chat
432
  if (streamController) {
433
  streamController.abort();
 
707
  });
708
  };
709
 
710
+ // Get all available MCP tools
711
+ const mcpTools = getAllTools();
712
+
713
  return (
714
  <div className="chat-window relative">
715
  {/* Floating New Chat button */}
 
809
 
810
  <div className="chat-input">
811
  <div className="flex items-center gap-2 w-full">
812
+ {mcpTools.length > 0 && (
813
+ <button
814
+ className="mcp-tools-button"
815
+ onClick={() => setShowMcpTools(!showMcpTools)}
816
+ title="MCP Tools"
817
+ >
818
+ 🧰
819
+ </button>
820
+ )}
821
+
822
  <textarea
823
  className="message-input"
824
  value={input}
 
839
  >
840
  Send
841
  </button>
 
842
  </div>
843
  </div>
844
+
845
+ {showMcpTools && (
846
+ <div className="mcp-tools-panel">
847
+ <h3>Available MCP Tools</h3>
848
+ <div className="mcp-tools-list">
849
+ {mcpTools.map((tool, index) => (
850
+ <div key={`${tool.serverId}-${tool.name}-${index}`} className="mcp-tool-item">
851
+ <div className="mcp-tool-header">
852
+ <strong>{tool.name}</strong>
853
+ <span className="mcp-server-name">({tool.serverName})</span>
854
+ </div>
855
+ <p className="mcp-tool-description">{tool.description}</p>
856
+ <button
857
+ className="mcp-tool-execute-button"
858
+ onClick={() => {
859
+ // For simplicity, we're not implementing parameter input UI
860
+ // In a real implementation, you would show a form for parameters
861
+ const parameters = {};
862
+ handleExecuteMcpTool(tool.serverId, tool.name, parameters);
863
+ }}
864
+ >
865
+ Execute
866
+ </button>
867
+ </div>
868
+ ))}
869
+ {mcpTools.length === 0 && (
870
+ <p className="no-tools-message">No MCP tools available. Add MCP servers in settings.</p>
871
+ )}
872
+ </div>
873
+ <button
874
+ className="close-mcp-tools-button"
875
+ onClick={() => setShowMcpTools(false)}
876
+ >
877
+ Close
878
+ </button>
879
+ </div>
880
+ )}
881
  </div>
882
  </div>
883
  );
src/components/Settings.jsx CHANGED
@@ -1,13 +1,15 @@
1
  import { useState } from 'react';
2
 
3
- function Settings({
4
- profiles,
5
- activeProfileId,
6
- onSaveProfiles,
7
- onChangeActiveProfile,
8
  onCloseSettings,
9
  onSaveSummarizationProfile,
10
- summarizationProfile
 
 
11
  }) {
12
  const [localProfiles, setLocalProfiles] = useState(profiles);
13
  const [localSummarizationProfile, setLocalSummarizationProfile] = useState(summarizationProfile || {
@@ -16,14 +18,17 @@ function Settings({
16
  apiKey: '',
17
  model: 'DeepSeek-R1'
18
  });
 
19
  const [editingProfileId, setEditingProfileId] = useState(activeProfileId);
 
20
  const [isHintExpanded, setIsHintExpanded] = useState(false);
 
21
 
22
  const editingProfile = localProfiles.find(p => p.id === editingProfileId) || localProfiles[0];
23
- const editingSummarizationProfile = localSummarizationProfile;
24
 
25
  const handleProfileChange = (updatedProfile) => {
26
- setLocalProfiles(localProfiles.map(p =>
27
  p.id === updatedProfile.id ? updatedProfile : p
28
  ));
29
  };
@@ -32,6 +37,12 @@ function Settings({
32
  setLocalSummarizationProfile(updatedProfile);
33
  };
34
 
 
 
 
 
 
 
35
  const handleAddProfile = () => {
36
  const newId = `profile-${Date.now()}`;
37
  const newProfile = {
@@ -41,7 +52,7 @@ function Settings({
41
  apiKey: '',
42
  model: 'DeepSeek-R1'
43
  };
44
-
45
  const updatedProfiles = [...localProfiles, newProfile];
46
  setLocalProfiles(updatedProfiles);
47
  setEditingProfileId(newId);
@@ -55,16 +66,43 @@ function Settings({
55
 
56
  const updatedProfiles = localProfiles.filter(p => p.id !== profileId);
57
  setLocalProfiles(updatedProfiles);
58
-
59
  if (editingProfileId === profileId) {
60
  setEditingProfileId(updatedProfiles[0].id);
61
  }
62
  };
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  const handleSubmit = (e) => {
65
  e.preventDefault();
66
  onSaveProfiles(localProfiles);
67
  onSaveSummarizationProfile(localSummarizationProfile);
 
 
 
68
  onChangeActiveProfile(editingProfileId);
69
  onCloseSettings();
70
  };
@@ -72,29 +110,29 @@ function Settings({
72
  return (
73
  <div className="settings-panel">
74
  <h2>Settings</h2>
75
-
76
  <div className="profiles-section">
77
  <div className="profiles-header">
78
  <h3>Chat Profiles</h3>
79
- <button
80
- type="button"
81
  className="add-profile-button"
82
  onClick={handleAddProfile}
83
  >
84
  + Add Profile
85
  </button>
86
  </div>
87
-
88
  <div className="profiles-list">
89
  {localProfiles.map(profile => (
90
- <div
91
- key={profile.id}
92
  className={`profile-item ${profile.id === editingProfileId ? 'active' : ''}`}
93
  onClick={() => setEditingProfileId(profile.id)}
94
  >
95
  <span>{profile.name}</span>
96
  {localProfiles.length > 1 && (
97
- <button
98
  className="delete-profile-button"
99
  onClick={(e) => {
100
  e.stopPropagation();
@@ -113,7 +151,7 @@ function Settings({
113
  {editingProfile && (
114
  <div className="current-profile-section">
115
  <h3>Edit Chat Profile: {editingProfile.name}</h3>
116
-
117
  <div className="setting-item">
118
  <label>Profile Name:</label>
119
  <input
@@ -121,11 +159,12 @@ function Settings({
121
  value={editingProfile.name}
122
  onChange={(e) => handleProfileChange({
123
  ...editingProfile,
124
- })}
 
125
  placeholder="Enter profile name"
126
  />
127
  </div>
128
-
129
  <div className="setting-item">
130
  <label>API Endpoint:</label>
131
  <input
@@ -138,8 +177,8 @@ function Settings({
138
  placeholder="Enter API endpoint"
139
  />
140
  <div className="setting-hint">
141
- <button
142
- type="button"
143
  className="hint-toggle"
144
  onClick={() => setIsHintExpanded(!isHintExpanded)}
145
  >
@@ -155,7 +194,7 @@ function Settings({
155
  </div>
156
  </div>
157
  </div>
158
-
159
  <div className="setting-item">
160
  <label>API Key:</label>
161
  <input
@@ -168,7 +207,7 @@ function Settings({
168
  placeholder="Enter your API key"
169
  />
170
  </div>
171
-
172
  <div className="setting-item">
173
  <label>Model:</label>
174
  <input
@@ -184,7 +223,7 @@ function Settings({
184
  </div>
185
  )}
186
 
187
-
188
  <div className="profiles-section">
189
  <h3>Summarization Profile</h3>
190
  <div className="current-profile-section">
@@ -200,7 +239,7 @@ function Settings({
200
  placeholder="Enter API endpoint"
201
  />
202
  </div>
203
-
204
  <div className="setting-item">
205
  <label>API Key:</label>
206
  <input
@@ -213,7 +252,7 @@ function Settings({
213
  placeholder="Enter your API key"
214
  />
215
  </div>
216
-
217
  <div className="setting-item">
218
  <label>Model:</label>
219
  <input
@@ -229,6 +268,122 @@ function Settings({
229
  </div>
230
  </div>
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  <div className="settings-actions">
233
  <button type="submit" className="save-button">Save Settings</button>
234
  <button type="button" className="cancel-button" onClick={onCloseSettings}>Cancel</button>
 
1
  import { useState } from 'react';
2
 
3
+ function Settings({
4
+ profiles,
5
+ activeProfileId,
6
+ onSaveProfiles,
7
+ onChangeActiveProfile,
8
  onCloseSettings,
9
  onSaveSummarizationProfile,
10
+ summarizationProfile,
11
+ mcpServers,
12
+ onSaveMcpServers
13
  }) {
14
  const [localProfiles, setLocalProfiles] = useState(profiles);
15
  const [localSummarizationProfile, setLocalSummarizationProfile] = useState(summarizationProfile || {
 
18
  apiKey: '',
19
  model: 'DeepSeek-R1'
20
  });
21
+ const [localMcpServers, setLocalMcpServers] = useState(mcpServers || []);
22
  const [editingProfileId, setEditingProfileId] = useState(activeProfileId);
23
+ const [editingMcpServerId, setEditingMcpServerId] = useState(null);
24
  const [isHintExpanded, setIsHintExpanded] = useState(false);
25
+ const [isMcpHintExpanded, setIsMcpHintExpanded] = useState(false);
26
 
27
  const editingProfile = localProfiles.find(p => p.id === editingProfileId) || localProfiles[0];
28
+ const editingMcpServer = localMcpServers.find(s => s.id === editingMcpServerId) || null;
29
 
30
  const handleProfileChange = (updatedProfile) => {
31
+ setLocalProfiles(localProfiles.map(p =>
32
  p.id === updatedProfile.id ? updatedProfile : p
33
  ));
34
  };
 
37
  setLocalSummarizationProfile(updatedProfile);
38
  };
39
 
40
+ const handleMcpServerChange = (updatedServer) => {
41
+ setLocalMcpServers(localMcpServers.map(s =>
42
+ s.id === updatedServer.id ? updatedServer : s
43
+ ));
44
+ };
45
+
46
  const handleAddProfile = () => {
47
  const newId = `profile-${Date.now()}`;
48
  const newProfile = {
 
52
  apiKey: '',
53
  model: 'DeepSeek-R1'
54
  };
55
+
56
  const updatedProfiles = [...localProfiles, newProfile];
57
  setLocalProfiles(updatedProfiles);
58
  setEditingProfileId(newId);
 
66
 
67
  const updatedProfiles = localProfiles.filter(p => p.id !== profileId);
68
  setLocalProfiles(updatedProfiles);
69
+
70
  if (editingProfileId === profileId) {
71
  setEditingProfileId(updatedProfiles[0].id);
72
  }
73
  };
74
 
75
+ const handleAddMcpServer = () => {
76
+ const newId = `mcp-server-${Date.now()}`;
77
+ const newServer = {
78
+ id: newId,
79
+ name: `MCP Server ${localMcpServers.length + 1}`,
80
+ endpoint: '',
81
+ authToken: '',
82
+ description: ''
83
+ };
84
+
85
+ const updatedServers = [...localMcpServers, newServer];
86
+ setLocalMcpServers(updatedServers);
87
+ setEditingMcpServerId(newId);
88
+ };
89
+
90
+ const handleDeleteMcpServer = (serverId) => {
91
+ const updatedServers = localMcpServers.filter(s => s.id !== serverId);
92
+ setLocalMcpServers(updatedServers);
93
+
94
+ if (editingMcpServerId === serverId) {
95
+ setEditingMcpServerId(updatedServers.length > 0 ? updatedServers[0].id : null);
96
+ }
97
+ };
98
+
99
  const handleSubmit = (e) => {
100
  e.preventDefault();
101
  onSaveProfiles(localProfiles);
102
  onSaveSummarizationProfile(localSummarizationProfile);
103
+ if (onSaveMcpServers) {
104
+ onSaveMcpServers(localMcpServers);
105
+ }
106
  onChangeActiveProfile(editingProfileId);
107
  onCloseSettings();
108
  };
 
110
  return (
111
  <div className="settings-panel">
112
  <h2>Settings</h2>
113
+
114
  <div className="profiles-section">
115
  <div className="profiles-header">
116
  <h3>Chat Profiles</h3>
117
+ <button
118
+ type="button"
119
  className="add-profile-button"
120
  onClick={handleAddProfile}
121
  >
122
  + Add Profile
123
  </button>
124
  </div>
125
+
126
  <div className="profiles-list">
127
  {localProfiles.map(profile => (
128
+ <div
129
+ key={profile.id}
130
  className={`profile-item ${profile.id === editingProfileId ? 'active' : ''}`}
131
  onClick={() => setEditingProfileId(profile.id)}
132
  >
133
  <span>{profile.name}</span>
134
  {localProfiles.length > 1 && (
135
+ <button
136
  className="delete-profile-button"
137
  onClick={(e) => {
138
  e.stopPropagation();
 
151
  {editingProfile && (
152
  <div className="current-profile-section">
153
  <h3>Edit Chat Profile: {editingProfile.name}</h3>
154
+
155
  <div className="setting-item">
156
  <label>Profile Name:</label>
157
  <input
 
159
  value={editingProfile.name}
160
  onChange={(e) => handleProfileChange({
161
  ...editingProfile,
162
+ name: e.target.value
163
+ })}
164
  placeholder="Enter profile name"
165
  />
166
  </div>
167
+
168
  <div className="setting-item">
169
  <label>API Endpoint:</label>
170
  <input
 
177
  placeholder="Enter API endpoint"
178
  />
179
  <div className="setting-hint">
180
+ <button
181
+ type="button"
182
  className="hint-toggle"
183
  onClick={() => setIsHintExpanded(!isHintExpanded)}
184
  >
 
194
  </div>
195
  </div>
196
  </div>
197
+
198
  <div className="setting-item">
199
  <label>API Key:</label>
200
  <input
 
207
  placeholder="Enter your API key"
208
  />
209
  </div>
210
+
211
  <div className="setting-item">
212
  <label>Model:</label>
213
  <input
 
223
  </div>
224
  )}
225
 
226
+
227
  <div className="profiles-section">
228
  <h3>Summarization Profile</h3>
229
  <div className="current-profile-section">
 
239
  placeholder="Enter API endpoint"
240
  />
241
  </div>
242
+
243
  <div className="setting-item">
244
  <label>API Key:</label>
245
  <input
 
252
  placeholder="Enter your API key"
253
  />
254
  </div>
255
+
256
  <div className="setting-item">
257
  <label>Model:</label>
258
  <input
 
268
  </div>
269
  </div>
270
 
271
+ {/* MCP Servers Section */}
272
+ <div className="profiles-section">
273
+ <div className="profiles-header">
274
+ <h3>MCP Servers</h3>
275
+ <button
276
+ type="button"
277
+ className="add-profile-button"
278
+ onClick={handleAddMcpServer}
279
+ >
280
+ + Add MCP Server
281
+ </button>
282
+ </div>
283
+
284
+ <div className="profiles-list">
285
+ {localMcpServers.map(server => (
286
+ <div
287
+ key={server.id}
288
+ className={`profile-item ${server.id === editingMcpServerId ? 'active' : ''}`}
289
+ onClick={() => setEditingMcpServerId(server.id)}
290
+ >
291
+ <span>{server.name}</span>
292
+ <button
293
+ className="delete-profile-button"
294
+ onClick={(e) => {
295
+ e.stopPropagation();
296
+ handleDeleteMcpServer(server.id);
297
+ }}
298
+ >
299
+ ×
300
+ </button>
301
+ </div>
302
+ ))}
303
+ {localMcpServers.length === 0 && (
304
+ <div className="empty-state">
305
+ <p>No MCP servers added yet. Add a server to enable Model Context Protocol capabilities.</p>
306
+ </div>
307
+ )}
308
+ </div>
309
+
310
+ {editingMcpServer && (
311
+ <div className="current-profile-section">
312
+ <h3>Edit MCP Server: {editingMcpServer.name}</h3>
313
+
314
+ <div className="setting-item">
315
+ <label>Server Name:</label>
316
+ <input
317
+ type="text"
318
+ value={editingMcpServer.name}
319
+ onChange={(e) => handleMcpServerChange({
320
+ ...editingMcpServer,
321
+ name: e.target.value
322
+ })}
323
+ placeholder="Enter server name"
324
+ />
325
+ </div>
326
+
327
+ <div className="setting-item">
328
+ <label>Endpoint URL:</label>
329
+ <input
330
+ type="text"
331
+ value={editingMcpServer.endpoint}
332
+ onChange={(e) => handleMcpServerChange({
333
+ ...editingMcpServer,
334
+ endpoint: e.target.value
335
+ })}
336
+ placeholder="Enter MCP server endpoint URL"
337
+ />
338
+ </div>
339
+
340
+ <div className="setting-item">
341
+ <label>Authentication Token:</label>
342
+ <input
343
+ type="password"
344
+ value={editingMcpServer.authToken}
345
+ onChange={(e) => handleMcpServerChange({
346
+ ...editingMcpServer,
347
+ authToken: e.target.value
348
+ })}
349
+ placeholder="Enter authentication token (if required)"
350
+ />
351
+ </div>
352
+
353
+ <div className="setting-item">
354
+ <label>Description:</label>
355
+ <textarea
356
+ value={editingMcpServer.description}
357
+ onChange={(e) => handleMcpServerChange({
358
+ ...editingMcpServer,
359
+ description: e.target.value
360
+ })}
361
+ placeholder="Enter server description"
362
+ rows="3"
363
+ />
364
+ </div>
365
+
366
+ <div className="setting-hint">
367
+ <button
368
+ type="button"
369
+ className="hint-toggle"
370
+ onClick={() => setIsMcpHintExpanded(!isMcpHintExpanded)}
371
+ >
372
+ {isMcpHintExpanded ? 'Hide' : 'Show'} MCP Information
373
+ </button>
374
+ <div className={`hint-content ${isMcpHintExpanded ? 'expanded' : ''}`}>
375
+ <p>Model Context Protocol (MCP) allows AI models to access external tools and data sources.</p>
376
+ <ul>
377
+ <li>MCP servers provide specialized capabilities to AI models</li>
378
+ <li>Each server can offer different tools like web search, data retrieval, etc.</li>
379
+ <li>The model will automatically use available MCP servers when needed</li>
380
+ </ul>
381
+ </div>
382
+ </div>
383
+ </div>
384
+ )}
385
+ </div>
386
+
387
  <div className="settings-actions">
388
  <button type="submit" className="save-button">Save Settings</button>
389
  <button type="button" className="cancel-button" onClick={onCloseSettings}>Cancel</button>
src/hooks/useMcp.js ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useEffect, useCallback } from 'react';
2
+
3
+ // Custom hook for managing MCP servers and tools
4
+ function useMcp(mcpServers) {
5
+ const [serverTools, setServerTools] = useState({});
6
+ const [isLoading, setIsLoading] = useState(false);
7
+ const [error, setError] = useState(null);
8
+
9
+ // Discover tools from all MCP servers
10
+ const discoverTools = useCallback(async () => {
11
+ console.log('Discovering MCP tools, servers:', mcpServers);
12
+ if (!mcpServers || mcpServers.length === 0) {
13
+ console.log('No MCP servers configured');
14
+ setServerTools({});
15
+ return;
16
+ }
17
+
18
+ setIsLoading(true);
19
+ setError(null);
20
+
21
+ const toolsMap = {};
22
+
23
+ try {
24
+ // Discover tools from each server in parallel
25
+ const discoveryPromises = mcpServers.map(async (server) => {
26
+ try {
27
+ const response = await fetch('/api/mcp/discover', {
28
+ method: 'POST',
29
+ headers: {
30
+ 'Content-Type': 'application/json',
31
+ },
32
+ body: JSON.stringify({ server })
33
+ });
34
+
35
+ if (!response.ok) {
36
+ const errorText = await response.text();
37
+ console.error(`Error discovering tools for server ${server.name}:`, errorText);
38
+ return {
39
+ serverId: server.id,
40
+ tools: [],
41
+ error: `Failed to discover tools: ${response.status}`
42
+ };
43
+ }
44
+
45
+ return await response.json();
46
+ } catch (error) {
47
+ console.error(`Error discovering tools for server ${server.name}:`, error);
48
+ return {
49
+ serverId: server.id,
50
+ tools: [],
51
+ error: error.message
52
+ };
53
+ }
54
+ });
55
+
56
+ const results = await Promise.all(discoveryPromises);
57
+
58
+ // Organize tools by server
59
+ results.forEach(result => {
60
+ if (result && result.serverId) {
61
+ toolsMap[result.serverId] = {
62
+ tools: result.tools || [],
63
+ error: result.error || null
64
+ };
65
+ }
66
+ });
67
+
68
+ setServerTools(toolsMap);
69
+ } catch (error) {
70
+ console.error('Error discovering MCP tools:', error);
71
+ setError(error.message);
72
+ } finally {
73
+ setIsLoading(false);
74
+ }
75
+ }, [mcpServers]);
76
+
77
+ // Execute a tool on an MCP server
78
+ const executeTool = useCallback(async (serverId, toolName, parameters) => {
79
+ if (!mcpServers || !serverId || !toolName) {
80
+ throw new Error('Invalid tool execution parameters');
81
+ }
82
+
83
+ const server = mcpServers.find(s => s.id === serverId);
84
+ if (!server) {
85
+ throw new Error(`MCP server with ID ${serverId} not found`);
86
+ }
87
+
88
+ try {
89
+ const response = await fetch('/api/mcp/execute', {
90
+ method: 'POST',
91
+ headers: {
92
+ 'Content-Type': 'application/json',
93
+ },
94
+ body: JSON.stringify({
95
+ server,
96
+ tool: toolName,
97
+ parameters
98
+ })
99
+ });
100
+
101
+ if (!response.ok) {
102
+ const errorText = await response.text();
103
+ throw new Error(`Tool execution failed: ${response.status} - ${errorText}`);
104
+ }
105
+
106
+ return await response.json();
107
+ } catch (error) {
108
+ console.error(`Error executing tool ${toolName} on server ${server.name}:`, error);
109
+ throw error;
110
+ }
111
+ }, [mcpServers]);
112
+
113
+ // Discover tools when servers change
114
+ useEffect(() => {
115
+ console.log('MCP servers changed, discovering tools');
116
+ discoverTools();
117
+ }, [discoverTools]);
118
+
119
+ // Get all available tools across all servers
120
+ const getAllTools = useCallback(() => {
121
+ console.log('Getting all MCP tools, serverTools:', serverTools);
122
+ const allTools = [];
123
+
124
+ Object.entries(serverTools).forEach(([serverId, serverData]) => {
125
+ if (serverData.tools && serverData.tools.length > 0) {
126
+ const server = mcpServers.find(s => s.id === serverId);
127
+ if (server) {
128
+ serverData.tools.forEach(tool => {
129
+ allTools.push({
130
+ ...tool,
131
+ serverId,
132
+ serverName: server.name
133
+ });
134
+ });
135
+ }
136
+ }
137
+ });
138
+
139
+ console.log('All MCP tools:', allTools);
140
+ return allTools;
141
+ }, [serverTools, mcpServers]);
142
+
143
+ return {
144
+ serverTools,
145
+ isLoading,
146
+ error,
147
+ discoverTools,
148
+ executeTool,
149
+ getAllTools
150
+ };
151
+ }
152
+
153
+ export default useMcp;
src/index.css CHANGED
@@ -1,3 +1,6 @@
 
 
 
1
  @tailwind base;
2
  @tailwind components;
3
  @tailwind utilities;
 
1
+ /* Import MCP styles */
2
+ @import './styles/mcp.css';
3
+
4
  @tailwind base;
5
  @tailwind components;
6
  @tailwind utilities;
src/styles/mcp.css ADDED
@@ -0,0 +1,133 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* MCP Tools Styling */
2
+
3
+ .mcp-tools-button {
4
+ display: flex;
5
+ align-items: center;
6
+ justify-content: center;
7
+ width: 36px;
8
+ height: 36px;
9
+ border-radius: 4px;
10
+ background-color: #f0f0f0;
11
+ border: 1px solid #ddd;
12
+ cursor: pointer;
13
+ font-size: 18px;
14
+ transition: background-color 0.2s;
15
+ }
16
+
17
+ .mcp-tools-button:hover {
18
+ background-color: #e0e0e0;
19
+ }
20
+
21
+ .mcp-tools-panel {
22
+ margin-top: 10px;
23
+ padding: 15px;
24
+ border: 1px solid #ddd;
25
+ border-radius: 6px;
26
+ background-color: #f9f9f9;
27
+ }
28
+
29
+ .mcp-tools-panel h3 {
30
+ margin-top: 0;
31
+ margin-bottom: 10px;
32
+ font-size: 16px;
33
+ font-weight: 600;
34
+ }
35
+
36
+ .mcp-tools-list {
37
+ display: grid;
38
+ grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
39
+ gap: 12px;
40
+ max-height: 300px;
41
+ overflow-y: auto;
42
+ padding-right: 5px;
43
+ }
44
+
45
+ .mcp-tool-item {
46
+ padding: 10px;
47
+ border: 1px solid #ddd;
48
+ border-radius: 4px;
49
+ background-color: white;
50
+ }
51
+
52
+ .mcp-tool-header {
53
+ display: flex;
54
+ align-items: center;
55
+ margin-bottom: 5px;
56
+ }
57
+
58
+ .mcp-server-name {
59
+ margin-left: 5px;
60
+ font-size: 12px;
61
+ color: #666;
62
+ }
63
+
64
+ .mcp-tool-description {
65
+ font-size: 13px;
66
+ color: #444;
67
+ margin-bottom: 10px;
68
+ }
69
+
70
+ .mcp-tool-execute-button {
71
+ padding: 5px 10px;
72
+ background-color: #3e6ae1;
73
+ color: white;
74
+ border: none;
75
+ border-radius: 4px;
76
+ cursor: pointer;
77
+ font-size: 13px;
78
+ }
79
+
80
+ .mcp-tool-execute-button:hover {
81
+ background-color: #2a56c8;
82
+ }
83
+
84
+ .close-mcp-tools-button {
85
+ margin-top: 10px;
86
+ padding: 5px 10px;
87
+ background-color: #f0f0f0;
88
+ border: 1px solid #ddd;
89
+ border-radius: 4px;
90
+ cursor: pointer;
91
+ font-size: 13px;
92
+ }
93
+
94
+ .close-mcp-tools-button:hover {
95
+ background-color: #e0e0e0;
96
+ }
97
+
98
+ .no-tools-message {
99
+ color: #666;
100
+ font-style: italic;
101
+ text-align: center;
102
+ padding: 20px;
103
+ }
104
+
105
+ /* MCP Message Styling */
106
+ .message .mcp-tool-execution {
107
+ background-color: #f0f8ff;
108
+ border-left: 3px solid #3e6ae1;
109
+ padding: 8px 12px;
110
+ margin-bottom: 5px;
111
+ font-family: monospace;
112
+ font-size: 13px;
113
+ }
114
+
115
+ .message .mcp-result {
116
+ background-color: #f5f5f5;
117
+ border-left: 3px solid #4caf50;
118
+ padding: 8px 12px;
119
+ margin-top: 5px;
120
+ font-family: monospace;
121
+ font-size: 13px;
122
+ white-space: pre-wrap;
123
+ overflow-x: auto;
124
+ }
125
+
126
+ .message .mcp-error {
127
+ background-color: #fff0f0;
128
+ border-left: 3px solid #f44336;
129
+ padding: 8px 12px;
130
+ margin-top: 5px;
131
+ font-family: monospace;
132
+ font-size: 13px;
133
+ }