From 2f7726b0081645356d2a7a2edf029c8cc21b5c73 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 26 Oct 2025 19:13:06 +0800 Subject: [PATCH] Add JSON import for MCP servers (#385) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: samanhappy <2755122+samanhappy@users.noreply.github.com> Co-authored-by: samanhappy --- frontend/src/components/JSONImportForm.tsx | 311 +++++++++++++++++++++ frontend/src/pages/ServersPage.tsx | 24 ++ locales/en.json | 16 ++ locales/fr.json | 16 ++ locales/zh.json | 16 ++ 5 files changed, 383 insertions(+) create mode 100644 frontend/src/components/JSONImportForm.tsx diff --git a/frontend/src/components/JSONImportForm.tsx b/frontend/src/components/JSONImportForm.tsx new file mode 100644 index 0000000..d624760 --- /dev/null +++ b/frontend/src/components/JSONImportForm.tsx @@ -0,0 +1,311 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { apiPost } from '@/utils/fetchInterceptor'; + +interface JSONImportFormProps { + onSuccess: () => void; + onCancel: () => void; +} + +interface McpServerConfig { + command?: string; + args?: string[]; + env?: Record; + type?: string; + url?: string; + headers?: Record; +} + +interface ImportJsonFormat { + mcpServers: Record; +} + +const JSONImportForm: React.FC = ({ onSuccess, onCancel }) => { + const { t } = useTranslation(); + const [jsonInput, setJsonInput] = useState(''); + const [error, setError] = useState(null); + const [isImporting, setIsImporting] = useState(false); + const [previewServers, setPreviewServers] = useState | null>( + null, + ); + + const examplePlaceholder = `STDIO example: +{ + "mcpServers": { + "stdio-server-example": { + "command": "npx", + "args": ["-y", "mcp-server-example"] + } + } +} + +SSE example: +{ + "mcpServers": { + "sse-server-example": { + "type": "sse", + "url": "http://localhost:3000" + } + } +} + +HTTP example: +{ + "mcpServers": { + "http-server-example": { + "type": "streamable-http", + "url": "http://localhost:3001", + "headers": { + "Content-Type": "application/json", + "Authorization": "Bearer your-token" + } + } + } +}`; + + const parseAndValidateJson = (input: string): ImportJsonFormat | null => { + try { + const parsed = JSON.parse(input.trim()); + + // Validate structure + if (!parsed.mcpServers || typeof parsed.mcpServers !== 'object') { + setError(t('jsonImport.invalidFormat')); + return null; + } + + return parsed as ImportJsonFormat; + } catch (e) { + setError(t('jsonImport.parseError')); + return null; + } + }; + + const handlePreview = () => { + setError(null); + const parsed = parseAndValidateJson(jsonInput); + if (!parsed) return; + + const servers = Object.entries(parsed.mcpServers).map(([name, config]) => { + // Normalize config to MCPHub format + const normalizedConfig: any = {}; + + if (config.type === 'sse' || config.type === 'streamable-http') { + normalizedConfig.type = config.type; + normalizedConfig.url = config.url; + if (config.headers) { + normalizedConfig.headers = config.headers; + } + } else { + // Default to stdio + normalizedConfig.type = 'stdio'; + normalizedConfig.command = config.command; + normalizedConfig.args = config.args || []; + if (config.env) { + normalizedConfig.env = config.env; + } + } + + return { name, config: normalizedConfig }; + }); + + setPreviewServers(servers); + }; + + const handleImport = async () => { + if (!previewServers) return; + + setIsImporting(true); + setError(null); + + try { + let successCount = 0; + const errors: string[] = []; + + for (const server of previewServers) { + try { + const result = await apiPost('/servers', { + name: server.name, + config: server.config, + }); + + if (result.success) { + successCount++; + } else { + errors.push(`${server.name}: ${result.message || t('jsonImport.addFailed')}`); + } + } catch (err) { + errors.push( + `${server.name}: ${err instanceof Error ? err.message : t('jsonImport.addFailed')}`, + ); + } + } + + if (errors.length > 0) { + setError( + t('jsonImport.partialSuccess', { count: successCount, total: previewServers.length }) + + '\n' + + errors.join('\n'), + ); + } + + if (successCount > 0) { + onSuccess(); + } + } catch (err) { + console.error('Import error:', err); + setError(t('jsonImport.importFailed')); + } finally { + setIsImporting(false); + } + }; + + return ( +
+
+
+

{t('jsonImport.title')}

+ +
+ + {error && ( +
+

{error}

+
+ )} + + {!previewServers ? ( +
+
+ +