Spaces:
Runtime error
Runtime error
feat: init MCP server (#2)
Browse filesSigned-off-by: Jintao Zhang <zhangjintao9020@gmail.com>
- server/index.js +52 -4
- server/mcp.js +81 -0
- server/mock-mcp-server.js +99 -0
- server/start-mock-mcp.js +2 -0
- src/App.jsx +5 -0
- src/components/ChatWindow.jsx +417 -2
- src/components/Settings.jsx +182 -27
- src/hooks/useMcp.js +153 -0
- src/index.css +3 -0
- src/styles/mcp.css +133 -0
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
|
| 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 |
+
}
|