From 99b204f5bf55a068e46c9c8de8e52e2e9a6246b3 Mon Sep 17 00:00:00 2001 From: simos Date: Mon, 11 Aug 2025 14:05:31 +0300 Subject: [PATCH] feat: add JSON import support for MCP server configuration in ToolsSettings --- server/routes/mcp.js | 85 +++++++++ src/components/ToolsSettings.jsx | 291 ++++++++++++++++--------------- 2 files changed, 232 insertions(+), 144 deletions(-) diff --git a/server/routes/mcp.js b/server/routes/mcp.js index 6172291..1b27539 100644 --- a/server/routes/mcp.js +++ b/server/routes/mcp.js @@ -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 '' + 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 router.delete('/cli/remove/:name', async (req, res) => { try { diff --git a/src/components/ToolsSettings.jsx b/src/components/ToolsSettings.jsx index 8da52fe..c5e621d 100644 --- a/src/components/ToolsSettings.jsx +++ b/src/components/ToolsSettings.jsx @@ -32,16 +32,16 @@ function ToolsSettings({ isOpen, onClose }) { url: '', headers: {}, timeout: 30000 - } + }, + jsonInput: '', // For JSON import + importMode: 'form' // 'form' or 'json' }); const [mcpLoading, setMcpLoading] = useState(false); const [mcpTestResults, setMcpTestResults] = useState({}); - const [mcpConfigTestResult, setMcpConfigTestResult] = useState(null); - const [mcpConfigTesting, setMcpConfigTesting] = useState(false); - const [mcpConfigTested, setMcpConfigTested] = useState(false); const [mcpServerTools, setMcpServerTools] = useState({}); const [mcpToolsLoading, setMcpToolsLoading] = useState({}); const [activeTab, setActiveTab] = useState('tools'); + const [jsonValidationError, setJsonValidationError] = useState(''); // Common tool patterns 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') => { try { @@ -386,13 +362,13 @@ function ToolsSettings({ isOpen, onClose }) { url: '', headers: {}, timeout: 30000 - } + }, + jsonInput: '', + importMode: 'form' }); setEditingMcpServer(null); setShowMcpForm(false); - setMcpConfigTestResult(null); - setMcpConfigTested(false); - setMcpConfigTesting(false); + setJsonValidationError(''); }; const openMcpForm = (server = null) => { @@ -417,9 +393,40 @@ function ToolsSettings({ isOpen, onClose }) { setMcpLoading(true); try { - await saveMcpServer(mcpFormData); - resetMcpForm(); - setSaveStatus('success'); + 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); + resetMcpForm(); + setSaveStatus('success'); + } } catch (error) { alert(`Error: ${error.message}`); setSaveStatus('error'); @@ -485,28 +492,8 @@ function ToolsSettings({ isOpen, onClose }) { [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) => { switch (type) { @@ -1055,9 +1042,35 @@ function ToolsSettings({ isOpen, onClose }) {
+ {/* Import Mode Toggle */} +
+ + +
+ {/* Basic Info */}
-
+
@@ -1065,38 +1078,36 @@ function ToolsSettings({ isOpen, onClose }) { value={mcpFormData.name} onChange={(e) => { setMcpFormData(prev => ({...prev, name: e.target.value})); - setMcpConfigTestResult(null); - setMcpConfigTested(false); }} placeholder="my-server" required />
-
- - -
+ {mcpFormData.importMode === 'form' && ( +
+ + +
+ )}
{/* Scope is fixed to user - no selection needed */} {/* Show raw configuration details when editing */} - {editingMcpServer && mcpFormData.raw && ( + {editingMcpServer && mcpFormData.raw && mcpFormData.importMode === 'form' && (

Configuration Details (from {editingMcpServer.scope === 'global' ? '~/.claude.json' : 'project config'}) @@ -1107,8 +1118,59 @@ function ToolsSettings({ isOpen, onClose }) {

)} - {/* Transport-specific Config */} - {mcpFormData.type === 'stdio' && ( + {/* JSON Import Mode */} + {mcpFormData.importMode === 'json' && ( +
+
+ +