From e41fbcc26673e6841bc28e71d0653bff2d44429c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Fri, 27 Feb 2026 13:13:10 -0800 Subject: [PATCH 1/4] feat(mcp): add sim tab and json config input for mcp servers --- .../settings-modal/components/mcp/mcp.tsx | 380 +++++++++++++----- .../workflow-mcp-servers.tsx | 171 +++++--- 2 files changed, 413 insertions(+), 138 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/mcp.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/mcp.tsx index 8a6f8f8b64..822ba4cb8c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/mcp.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/mcp.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { ChevronDown, Plus, Search, X } from 'lucide-react' +import { Braces, ChevronDown, List, Plus, Search, X } from 'lucide-react' import { useParams } from 'next/navigation' import { Badge, @@ -13,6 +13,7 @@ import { ModalContent, ModalFooter, ModalHeader, + Textarea, Tooltip, } from '@/components/emcn' import { Input } from '@/components/ui' @@ -438,6 +439,9 @@ export function MCP({ initialServerId }: MCPProps) { const [showAddForm, setShowAddForm] = useState(false) const [formData, setFormData] = useState(DEFAULT_FORM_DATA) const [isAddingServer, setIsAddingServer] = useState(false) + const [addFormMode, setAddFormMode] = useState<'form' | 'json'>('form') + const [jsonInput, setJsonInput] = useState('') + const [jsonError, setJsonError] = useState(null) const [searchTerm, setSearchTerm] = useState('') const [deletingServers, setDeletingServers] = useState>(new Set()) @@ -501,6 +505,9 @@ export function MCP({ initialServerId }: MCPProps) { const resetForm = useCallback(() => { setFormData(DEFAULT_FORM_DATA) setShowAddForm(false) + setAddFormMode('form') + setJsonInput('') + setJsonError(null) resetEnvVarState() clearTestResult() }, [clearTestResult, resetEnvVarState]) @@ -650,6 +657,109 @@ export function MCP({ initialServerId }: MCPProps) { } }, [formData, testConnection, createServerMutation, workspaceId, headersToRecord, resetForm]) + /** + * Parses MCP JSON config into form data. + * Accepts both `{ mcpServers: { name: { url, headers } } }` and `{ url, headers }` formats. + */ + const parseJsonConfig = useCallback( + (json: string): { name: string; url: string; headers: Record } | null => { + try { + const parsed = JSON.parse(json) + + if (parsed.mcpServers && typeof parsed.mcpServers === 'object') { + const entries = Object.entries(parsed.mcpServers) + if (entries.length === 0) { + setJsonError('No servers found in mcpServers') + return null + } + const [name, config] = entries[0] as [string, Record] + if (!config.url || typeof config.url !== 'string') { + setJsonError('Server config must include a "url" field') + return null + } + setJsonError(null) + return { + name, + url: config.url, + headers: (config.headers as Record) || {}, + } + } + + if (parsed.url && typeof parsed.url === 'string') { + setJsonError(null) + return { + name: '', + url: parsed.url, + headers: (parsed.headers as Record) || {}, + } + } + + setJsonError('JSON must contain "mcpServers" or a "url" field') + return null + } catch { + setJsonError('Invalid JSON') + return null + } + }, + [] + ) + + /** + * Adds an MCP server from parsed JSON config. + */ + const handleAddServerFromJson = useCallback(async () => { + const config = parseJsonConfig(jsonInput) + if (!config) return + + if (!config.name) { + setJsonError( + 'Server name is required. Use the mcpServers format: { "mcpServers": { "name": { ... } } }' + ) + return + } + + setIsAddingServer(true) + try { + const serverConfig = { + name: config.name, + transport: 'streamable-http' as const, + url: config.url, + headers: config.headers, + timeout: 30000, + workspaceId, + } + + const connectionResult = await testConnection(serverConfig) + + if (!connectionResult.success) { + logger.error('Connection test failed, server not added:', connectionResult.error) + return + } + + await createServerMutation.mutateAsync({ + workspaceId, + config: { + name: config.name, + transport: 'streamable-http', + url: config.url, + timeout: 30000, + headers: config.headers, + enabled: true, + }, + }) + + logger.info(`Added MCP server from JSON: ${config.name}`) + setJsonInput('') + setJsonError(null) + setAddFormMode('form') + resetForm() + } catch (error) { + logger.error('Failed to add MCP server from JSON:', error) + } finally { + setIsAddingServer(false) + } + }, [jsonInput, parseJsonConfig, testConnection, createServerMutation, workspaceId, resetForm]) + /** * Opens the delete confirmation dialog for an MCP server. */ @@ -1458,102 +1568,190 @@ export function MCP({ initialServerId }: MCPProps) { {shouldShowForm && !serversLoading && (
- - { - if (testResult) clearTestResult() - handleNameChange(e.target.value) - }} - className='h-9' - /> - - - - handleInputChange('url', e.target.value)} - onScroll={(scrollLeft) => handleUrlScroll(scrollLeft)} - /> - {isAddDomainBlocked && ( -

- Domain not permitted by server policy -

- )} -
+
+ + + + + + {addFormMode === 'form' ? 'Switch to JSON' : 'Switch to form'} + + +
-
-
- - Headers - - -
+ {addFormMode === 'json' ? ( + <> +