mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-15 05:29:36 +00:00
feat: add JSON import support for MCP server configuration in ToolsSettings
This commit is contained in:
@@ -130,6 +130,91 @@ router.post('/cli/add', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// POST /api/mcp/cli/add-json - Add MCP server using JSON format
|
||||||
|
router.post('/cli/add-json', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { name, jsonConfig } = req.body;
|
||||||
|
|
||||||
|
console.log('➕ Adding MCP server using JSON format:', name);
|
||||||
|
|
||||||
|
// Validate and parse JSON config
|
||||||
|
let parsedConfig;
|
||||||
|
try {
|
||||||
|
parsedConfig = typeof jsonConfig === 'string' ? JSON.parse(jsonConfig) : jsonConfig;
|
||||||
|
} catch (parseError) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid JSON configuration',
|
||||||
|
details: parseError.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!parsedConfig.type) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid configuration',
|
||||||
|
details: 'Missing required field: type'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsedConfig.type === 'stdio' && !parsedConfig.command) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid configuration',
|
||||||
|
details: 'stdio type requires a command field'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((parsedConfig.type === 'http' || parsedConfig.type === 'sse') && !parsedConfig.url) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: 'Invalid configuration',
|
||||||
|
details: `${parsedConfig.type} type requires a url field`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
|
||||||
|
// Build the command: claude mcp add-json --scope user <name> '<json>'
|
||||||
|
const cliArgs = ['mcp', 'add-json', '--scope', 'user', name];
|
||||||
|
|
||||||
|
// Add the JSON config as a properly formatted string
|
||||||
|
const jsonString = JSON.stringify(parsedConfig);
|
||||||
|
cliArgs.push(jsonString);
|
||||||
|
|
||||||
|
console.log('🔧 Running Claude CLI command:', 'claude', cliArgs[0], cliArgs[1], cliArgs[2], cliArgs[3], cliArgs[4], jsonString);
|
||||||
|
|
||||||
|
const process = spawn('claude', cliArgs, {
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe']
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let stderr = '';
|
||||||
|
|
||||||
|
process.stdout.on('data', (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.stderr.on('data', (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
res.json({ success: true, output: stdout, message: `MCP server "${name}" added successfully via JSON` });
|
||||||
|
} else {
|
||||||
|
console.error('Claude CLI error:', stderr);
|
||||||
|
res.status(400).json({ error: 'Claude CLI command failed', details: stderr });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('error', (error) => {
|
||||||
|
console.error('Error running Claude CLI:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to run Claude CLI', details: error.message });
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding MCP server via JSON:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to add MCP server', details: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// DELETE /api/mcp/cli/remove/:name - Remove MCP server using Claude CLI
|
// DELETE /api/mcp/cli/remove/:name - Remove MCP server using Claude CLI
|
||||||
router.delete('/cli/remove/:name', async (req, res) => {
|
router.delete('/cli/remove/:name', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -32,16 +32,16 @@ function ToolsSettings({ isOpen, onClose }) {
|
|||||||
url: '',
|
url: '',
|
||||||
headers: {},
|
headers: {},
|
||||||
timeout: 30000
|
timeout: 30000
|
||||||
}
|
},
|
||||||
|
jsonInput: '', // For JSON import
|
||||||
|
importMode: 'form' // 'form' or 'json'
|
||||||
});
|
});
|
||||||
const [mcpLoading, setMcpLoading] = useState(false);
|
const [mcpLoading, setMcpLoading] = useState(false);
|
||||||
const [mcpTestResults, setMcpTestResults] = useState({});
|
const [mcpTestResults, setMcpTestResults] = useState({});
|
||||||
const [mcpConfigTestResult, setMcpConfigTestResult] = useState(null);
|
|
||||||
const [mcpConfigTesting, setMcpConfigTesting] = useState(false);
|
|
||||||
const [mcpConfigTested, setMcpConfigTested] = useState(false);
|
|
||||||
const [mcpServerTools, setMcpServerTools] = useState({});
|
const [mcpServerTools, setMcpServerTools] = useState({});
|
||||||
const [mcpToolsLoading, setMcpToolsLoading] = useState({});
|
const [mcpToolsLoading, setMcpToolsLoading] = useState({});
|
||||||
const [activeTab, setActiveTab] = useState('tools');
|
const [activeTab, setActiveTab] = useState('tools');
|
||||||
|
const [jsonValidationError, setJsonValidationError] = useState('');
|
||||||
|
|
||||||
// Common tool patterns
|
// Common tool patterns
|
||||||
const commonTools = [
|
const commonTools = [
|
||||||
@@ -234,30 +234,6 @@ function ToolsSettings({ isOpen, onClose }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const testMcpConfiguration = async (formData) => {
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('auth-token');
|
|
||||||
const response = await fetch('/api/mcp/servers/test', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Authorization': `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(formData)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const data = await response.json();
|
|
||||||
return data.testResult;
|
|
||||||
} else {
|
|
||||||
const error = await response.json();
|
|
||||||
throw new Error(error.error || 'Failed to test configuration');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error testing MCP configuration:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const discoverMcpTools = async (serverId, scope = 'user') => {
|
const discoverMcpTools = async (serverId, scope = 'user') => {
|
||||||
try {
|
try {
|
||||||
@@ -386,13 +362,13 @@ function ToolsSettings({ isOpen, onClose }) {
|
|||||||
url: '',
|
url: '',
|
||||||
headers: {},
|
headers: {},
|
||||||
timeout: 30000
|
timeout: 30000
|
||||||
}
|
},
|
||||||
|
jsonInput: '',
|
||||||
|
importMode: 'form'
|
||||||
});
|
});
|
||||||
setEditingMcpServer(null);
|
setEditingMcpServer(null);
|
||||||
setShowMcpForm(false);
|
setShowMcpForm(false);
|
||||||
setMcpConfigTestResult(null);
|
setJsonValidationError('');
|
||||||
setMcpConfigTested(false);
|
|
||||||
setMcpConfigTesting(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const openMcpForm = (server = null) => {
|
const openMcpForm = (server = null) => {
|
||||||
@@ -417,9 +393,40 @@ function ToolsSettings({ isOpen, onClose }) {
|
|||||||
setMcpLoading(true);
|
setMcpLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (mcpFormData.importMode === 'json') {
|
||||||
|
// Use JSON import endpoint
|
||||||
|
const token = localStorage.getItem('auth-token');
|
||||||
|
const response = await fetch('/api/mcp/cli/add-json', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: mcpFormData.name,
|
||||||
|
jsonConfig: mcpFormData.jsonInput
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.success) {
|
||||||
|
await fetchMcpServers(); // Refresh the list
|
||||||
|
resetMcpForm();
|
||||||
|
setSaveStatus('success');
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Failed to add server via JSON');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || 'Failed to add server');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use regular form-based save
|
||||||
await saveMcpServer(mcpFormData);
|
await saveMcpServer(mcpFormData);
|
||||||
resetMcpForm();
|
resetMcpForm();
|
||||||
setSaveStatus('success');
|
setSaveStatus('success');
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert(`Error: ${error.message}`);
|
alert(`Error: ${error.message}`);
|
||||||
setSaveStatus('error');
|
setSaveStatus('error');
|
||||||
@@ -485,28 +492,8 @@ function ToolsSettings({ isOpen, onClose }) {
|
|||||||
[key]: value
|
[key]: value
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
// Reset test status when configuration changes
|
|
||||||
setMcpConfigTestResult(null);
|
|
||||||
setMcpConfigTested(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTestConfiguration = async () => {
|
|
||||||
setMcpConfigTesting(true);
|
|
||||||
try {
|
|
||||||
const result = await testMcpConfiguration(mcpFormData);
|
|
||||||
setMcpConfigTestResult(result);
|
|
||||||
setMcpConfigTested(true);
|
|
||||||
} catch (error) {
|
|
||||||
setMcpConfigTestResult({
|
|
||||||
success: false,
|
|
||||||
message: error.message,
|
|
||||||
details: []
|
|
||||||
});
|
|
||||||
setMcpConfigTested(true);
|
|
||||||
} finally {
|
|
||||||
setMcpConfigTesting(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTransportIcon = (type) => {
|
const getTransportIcon = (type) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@@ -1055,9 +1042,35 @@ function ToolsSettings({ isOpen, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleMcpSubmit} className="p-4 space-y-4">
|
<form onSubmit={handleMcpSubmit} className="p-4 space-y-4">
|
||||||
|
{/* Import Mode Toggle */}
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMcpFormData(prev => ({...prev, importMode: 'form'}))}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||||
|
mcpFormData.importMode === 'form'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Form Input
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setMcpFormData(prev => ({...prev, importMode: 'json'}))}
|
||||||
|
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||||
|
mcpFormData.importMode === 'json'
|
||||||
|
? 'bg-blue-600 text-white'
|
||||||
|
: 'bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
JSON Import
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Basic Info */}
|
{/* Basic Info */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<div className={mcpFormData.importMode === 'json' ? 'md:col-span-2' : ''}>
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
Server Name *
|
Server Name *
|
||||||
</label>
|
</label>
|
||||||
@@ -1065,14 +1078,13 @@ function ToolsSettings({ isOpen, onClose }) {
|
|||||||
value={mcpFormData.name}
|
value={mcpFormData.name}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setMcpFormData(prev => ({...prev, name: e.target.value}));
|
setMcpFormData(prev => ({...prev, name: e.target.value}));
|
||||||
setMcpConfigTestResult(null);
|
|
||||||
setMcpConfigTested(false);
|
|
||||||
}}
|
}}
|
||||||
placeholder="my-server"
|
placeholder="my-server"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{mcpFormData.importMode === 'form' && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
Transport Type *
|
Transport Type *
|
||||||
@@ -1081,8 +1093,6 @@ function ToolsSettings({ isOpen, onClose }) {
|
|||||||
value={mcpFormData.type}
|
value={mcpFormData.type}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setMcpFormData(prev => ({...prev, type: e.target.value}));
|
setMcpFormData(prev => ({...prev, type: e.target.value}));
|
||||||
setMcpConfigTestResult(null);
|
|
||||||
setMcpConfigTested(false);
|
|
||||||
}}
|
}}
|
||||||
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500"
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500"
|
||||||
>
|
>
|
||||||
@@ -1091,12 +1101,13 @@ function ToolsSettings({ isOpen, onClose }) {
|
|||||||
<option value="http">HTTP</option>
|
<option value="http">HTTP</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scope is fixed to user - no selection needed */}
|
{/* Scope is fixed to user - no selection needed */}
|
||||||
|
|
||||||
{/* Show raw configuration details when editing */}
|
{/* Show raw configuration details when editing */}
|
||||||
{editingMcpServer && mcpFormData.raw && (
|
{editingMcpServer && mcpFormData.raw && mcpFormData.importMode === 'form' && (
|
||||||
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
<div className="bg-gray-50 dark:bg-gray-900/50 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||||
<h4 className="text-sm font-medium text-foreground mb-2">
|
<h4 className="text-sm font-medium text-foreground mb-2">
|
||||||
Configuration Details (from {editingMcpServer.scope === 'global' ? '~/.claude.json' : 'project config'})
|
Configuration Details (from {editingMcpServer.scope === 'global' ? '~/.claude.json' : 'project config'})
|
||||||
@@ -1107,8 +1118,59 @@ function ToolsSettings({ isOpen, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Transport-specific Config */}
|
{/* JSON Import Mode */}
|
||||||
{mcpFormData.type === 'stdio' && (
|
{mcpFormData.importMode === 'json' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
|
JSON Configuration *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={mcpFormData.jsonInput}
|
||||||
|
onChange={(e) => {
|
||||||
|
setMcpFormData(prev => ({...prev, jsonInput: e.target.value}));
|
||||||
|
// Validate JSON as user types
|
||||||
|
try {
|
||||||
|
if (e.target.value.trim()) {
|
||||||
|
const parsed = JSON.parse(e.target.value);
|
||||||
|
// Basic validation
|
||||||
|
if (!parsed.type) {
|
||||||
|
setJsonValidationError('Missing required field: type');
|
||||||
|
} else if (parsed.type === 'stdio' && !parsed.command) {
|
||||||
|
setJsonValidationError('stdio type requires a command field');
|
||||||
|
} else if ((parsed.type === 'http' || parsed.type === 'sse') && !parsed.url) {
|
||||||
|
setJsonValidationError(`${parsed.type} type requires a url field`);
|
||||||
|
} else {
|
||||||
|
setJsonValidationError('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (e.target.value.trim()) {
|
||||||
|
setJsonValidationError('Invalid JSON format');
|
||||||
|
} else {
|
||||||
|
setJsonValidationError('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`w-full px-3 py-2 border ${jsonValidationError ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'} bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 rounded-lg focus:ring-blue-500 focus:border-blue-500 font-mono text-sm`}
|
||||||
|
rows="8"
|
||||||
|
placeholder={'{\n "type": "stdio",\n "command": "/path/to/server",\n "args": ["--api-key", "abc123"],\n "env": {\n "CACHE_DIR": "/tmp"\n }\n}'}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{jsonValidationError && (
|
||||||
|
<p className="text-xs text-red-500 mt-1">{jsonValidationError}</p>
|
||||||
|
)}
|
||||||
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
|
Paste your MCP server configuration in JSON format. Example formats:
|
||||||
|
<br />• stdio: {`{"type":"stdio","command":"npx","args":["@upstash/context7-mcp"]}`}
|
||||||
|
<br />• http/sse: {`{"type":"http","url":"https://api.example.com/mcp"}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Transport-specific Config - Only show in form mode */}
|
||||||
|
{mcpFormData.importMode === 'form' && mcpFormData.type === 'stdio' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
@@ -1137,7 +1199,7 @@ function ToolsSettings({ isOpen, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(mcpFormData.type === 'sse' || mcpFormData.type === 'http') && (
|
{mcpFormData.importMode === 'form' && (mcpFormData.type === 'sse' || mcpFormData.type === 'http') && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
URL *
|
URL *
|
||||||
@@ -1152,7 +1214,8 @@ function ToolsSettings({ isOpen, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Environment Variables */}
|
{/* Environment Variables - Only show in form mode */}
|
||||||
|
{mcpFormData.importMode === 'form' && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
Environment Variables (KEY=value, one per line)
|
Environment Variables (KEY=value, one per line)
|
||||||
@@ -1174,8 +1237,9 @@ function ToolsSettings({ isOpen, onClose }) {
|
|||||||
placeholder="API_KEY=your-key DEBUG=true"
|
placeholder="API_KEY=your-key DEBUG=true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{(mcpFormData.type === 'sse' || mcpFormData.type === 'http') && (
|
{mcpFormData.importMode === 'form' && (mcpFormData.type === 'sse' || mcpFormData.type === 'http') && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-foreground mb-2">
|
<label className="block text-sm font-medium text-foreground mb-2">
|
||||||
Headers (KEY=value, one per line)
|
Headers (KEY=value, one per line)
|
||||||
@@ -1199,67 +1263,6 @@ function ToolsSettings({ isOpen, onClose }) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Test Configuration Section */}
|
|
||||||
<div className="border-t border-gray-200 dark:border-gray-700 pt-4">
|
|
||||||
<div className="flex items-center justify-between mb-3">
|
|
||||||
<h4 className="font-medium text-foreground">Configuration Test</h4>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={handleTestConfiguration}
|
|
||||||
disabled={mcpConfigTesting || !mcpFormData.name.trim()}
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
className="text-blue-600 border-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/20"
|
|
||||||
>
|
|
||||||
{mcpConfigTesting ? (
|
|
||||||
<>
|
|
||||||
<div className="w-4 h-4 animate-spin rounded-full border-2 border-blue-600 border-t-transparent mr-2" />
|
|
||||||
Testing...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Play className="w-4 h-4 mr-2" />
|
|
||||||
Test Configuration
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground mb-3">
|
|
||||||
You can test your configuration to verify it's working correctly.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{mcpConfigTestResult && (
|
|
||||||
<div className={`p-3 rounded-lg text-sm ${
|
|
||||||
mcpConfigTestResult.success
|
|
||||||
? 'bg-green-50 dark:bg-green-900/20 text-green-800 dark:text-green-200 border border-green-200 dark:border-green-800'
|
|
||||||
: 'bg-red-50 dark:bg-red-900/20 text-red-800 dark:text-red-200 border border-red-200 dark:border-red-800'
|
|
||||||
}`}>
|
|
||||||
<div className="font-medium flex items-center gap-2">
|
|
||||||
{mcpConfigTestResult.success ? (
|
|
||||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
{mcpConfigTestResult.message}
|
|
||||||
</div>
|
|
||||||
{mcpConfigTestResult.details && mcpConfigTestResult.details.length > 0 && (
|
|
||||||
<ul className="mt-2 space-y-1 text-xs">
|
|
||||||
{mcpConfigTestResult.details.map((detail, i) => (
|
|
||||||
<li key={i} className="flex items-start gap-1">
|
|
||||||
<span className="text-gray-400 mt-0.5">•</span>
|
|
||||||
<span>{detail}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-2 pt-4">
|
<div className="flex justify-end gap-2 pt-4">
|
||||||
<Button type="button" variant="outline" onClick={resetMcpForm}>
|
<Button type="button" variant="outline" onClick={resetMcpForm}>
|
||||||
|
|||||||
Reference in New Issue
Block a user