-
Notifications
You must be signed in to change notification settings - Fork 489
fix: enable MCP server management in web mode (CRITICAL) #320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1f493d2
3fbe662
fa0d99c
e560949
e9bac50
5dda3ef
611d943
ee41434
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -276,8 +276,15 @@ export function useMCPServers() { | |
| // If editing an existing server, save directly (user already approved it) | ||
| if (editingServer) { | ||
| updateMCPServer(editingServer.id, serverData); | ||
|
|
||
| const syncSuccess = await syncSettingsToServer(); | ||
|
|
||
| if (!syncSuccess) { | ||
| toast.error('Failed to save MCP server to disk'); | ||
| return; | ||
| } | ||
|
|
||
| toast.success('MCP server updated'); | ||
| await syncSettingsToServer(); | ||
| handleCloseDialog(); | ||
| return; | ||
| } | ||
|
|
@@ -303,14 +310,28 @@ export function useMCPServers() { | |
|
|
||
| if (pendingServerData.type === 'add' && pendingServerData.serverData) { | ||
| addMCPServer(pendingServerData.serverData); | ||
|
|
||
| const syncSuccess = await syncSettingsToServer(); | ||
|
|
||
| if (!syncSuccess) { | ||
| toast.error('Failed to save MCP server to disk'); | ||
| return; | ||
| } | ||
|
|
||
| toast.success('MCP server added'); | ||
| await syncSettingsToServer(); | ||
| handleCloseDialog(); | ||
| } else if (pendingServerData.type === 'import' && pendingServerData.importServers) { | ||
| for (const serverData of pendingServerData.importServers) { | ||
| addMCPServer(serverData); | ||
| } | ||
| await syncSettingsToServer(); | ||
|
|
||
| const syncSuccess = await syncSettingsToServer(); | ||
|
|
||
| if (!syncSuccess) { | ||
| toast.error('Failed to save MCP servers to disk'); | ||
| return; | ||
| } | ||
|
|
||
| const count = pendingServerData.importServers.length; | ||
| toast.success(`Imported ${count} MCP server${count > 1 ? 's' : ''}`); | ||
| setIsImportDialogOpen(false); | ||
|
|
@@ -323,13 +344,27 @@ export function useMCPServers() { | |
|
|
||
| const handleToggleEnabled = async (server: MCPServerConfig) => { | ||
| updateMCPServer(server.id, { enabled: !server.enabled }); | ||
| await syncSettingsToServer(); | ||
|
|
||
| const syncSuccess = await syncSettingsToServer(); | ||
|
|
||
| if (!syncSuccess) { | ||
| toast.error('Failed to save MCP server to disk'); | ||
| return; | ||
| } | ||
|
|
||
| toast.success(server.enabled ? 'Server disabled' : 'Server enabled'); | ||
| }; | ||
|
|
||
| const handleDelete = async (id: string) => { | ||
| removeMCPServer(id); | ||
| await syncSettingsToServer(); | ||
|
|
||
| const syncSuccess = await syncSettingsToServer(); | ||
|
|
||
| if (!syncSuccess) { | ||
| toast.error('Failed to save changes to disk'); | ||
| return; | ||
| } | ||
|
|
||
| setDeleteConfirmId(null); | ||
| toast.success('MCP server removed'); | ||
| }; | ||
|
|
@@ -569,11 +604,11 @@ export function useMCPServers() { | |
| }; | ||
|
|
||
| const handleOpenGlobalJsonEdit = () => { | ||
| // Build the full mcpServers config object | ||
| const exportData: Record<string, Record<string, unknown>> = {}; | ||
|
|
||
| for (const server of mcpServers) { | ||
| // Build array of servers with IDs preserved | ||
| const serversArray = mcpServers.map((server) => { | ||
| const serverConfig: Record<string, unknown> = { | ||
| id: server.id, // Preserve ID | ||
| name: server.name, // Preserve name | ||
| type: server.type || 'stdio', | ||
| }; | ||
|
|
||
|
|
@@ -596,102 +631,199 @@ export function useMCPServers() { | |
| } | ||
| } | ||
|
|
||
| exportData[server.name] = serverConfig; | ||
| } | ||
| return serverConfig; | ||
| }); | ||
|
|
||
| setGlobalJsonValue(JSON.stringify({ mcpServers: exportData }, null, 2)); | ||
| setGlobalJsonValue(JSON.stringify({ mcpServers: serversArray }, null, 2)); | ||
| setIsGlobalJsonEditOpen(true); | ||
| }; | ||
|
|
||
| const handleSaveGlobalJsonEdit = async () => { | ||
| try { | ||
| const parsed = JSON.parse(globalJsonValue); | ||
|
|
||
| // Support both formats | ||
| // Support both formats: array and object | ||
| const servers = parsed.mcpServers || parsed; | ||
|
|
||
| if (typeof servers !== 'object' || Array.isArray(servers)) { | ||
| toast.error('Invalid format: expected object with server configurations'); | ||
| if (Array.isArray(servers)) { | ||
| // Array format (new format with IDs) | ||
| await handleSaveGlobalJsonArray(servers); | ||
| } else if (typeof servers === 'object' && servers !== null) { | ||
| // Object format (legacy Claude Desktop format) | ||
| await handleSaveGlobalJsonObject(servers); | ||
| } else { | ||
| toast.error('Invalid format: expected array or object with server configurations'); | ||
| return; | ||
| } | ||
|
|
||
| // Validate all servers first | ||
| for (const [name, config] of Object.entries(servers)) { | ||
| if (typeof config !== 'object' || config === null) { | ||
| toast.error(`Invalid config for "${name}"`); | ||
| return; | ||
| } | ||
| const syncSuccess = await syncSettingsToServer(); | ||
|
|
||
| const serverConfig = config as Record<string, unknown>; | ||
| const serverType = (serverConfig.type as string) || 'stdio'; | ||
| if (!syncSuccess) { | ||
| toast.error('Failed to save MCP servers to disk'); | ||
| return; | ||
| } | ||
|
|
||
| if (serverType === 'stdio') { | ||
| if (!serverConfig.command || typeof serverConfig.command !== 'string') { | ||
| toast.error(`Command is required for "${name}" (stdio)`); | ||
| return; | ||
| } | ||
| } else if (serverType === 'sse' || serverType === 'http') { | ||
| if (!serverConfig.url || typeof serverConfig.url !== 'string') { | ||
| toast.error(`URL is required for "${name}" (${serverType})`); | ||
| return; | ||
| } | ||
| toast.success('MCP servers configuration updated'); | ||
| setIsGlobalJsonEditOpen(false); | ||
| setGlobalJsonValue(''); | ||
| } catch (error) { | ||
| toast.error('Invalid JSON: ' + (error instanceof Error ? error.message : 'Parse error')); | ||
| } | ||
| }; | ||
|
|
||
| // Helper: Process array format (with IDs) | ||
| const handleSaveGlobalJsonArray = async (serversArray: unknown[]) => { | ||
| // Validate all servers first | ||
| for (const config of serversArray) { | ||
| if (typeof config !== 'object' || config === null) { | ||
| toast.error('Invalid server config in array'); | ||
| throw new Error('Invalid server config'); | ||
| } | ||
|
|
||
| const serverConfig = config as Record<string, unknown>; | ||
| const name = serverConfig.name as string; | ||
| const serverType = (serverConfig.type as string) || 'stdio'; | ||
|
|
||
| if (!name || typeof name !== 'string') { | ||
| toast.error('Server name is required'); | ||
| throw new Error('Server name required'); | ||
| } | ||
|
|
||
| if (serverType === 'stdio') { | ||
| if (!serverConfig.command || typeof serverConfig.command !== 'string') { | ||
| toast.error(`Command is required for "${name}" (stdio)`); | ||
| throw new Error('Command required'); | ||
| } | ||
| } else if (serverType === 'sse' || serverType === 'http') { | ||
| if (!serverConfig.url || typeof serverConfig.url !== 'string') { | ||
| toast.error(`URL is required for "${name}" (${serverType})`); | ||
| throw new Error('URL required'); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Create a map of existing servers by name for updating | ||
| const existingByName = new Map(mcpServers.map((s) => [s.name, s])); | ||
| const processedNames = new Set<string>(); | ||
| // Create maps of existing servers by ID and name | ||
| const existingById = new Map(mcpServers.map((s) => [s.id, s])); | ||
| const existingByName = new Map(mcpServers.map((s) => [s.name, s])); | ||
| const processedIds = new Set<string>(); | ||
|
|
||
| // Update or add servers | ||
| for (const [name, config] of Object.entries(servers)) { | ||
| const serverConfig = config as Record<string, unknown>; | ||
| const serverType = (serverConfig.type as ServerType) || 'stdio'; | ||
| // Update or add servers | ||
| for (const config of serversArray) { | ||
| const serverConfig = config as Record<string, unknown>; | ||
| const id = serverConfig.id as string | undefined; | ||
| const name = serverConfig.name as string; | ||
| const serverType = (serverConfig.type as ServerType) || 'stdio'; | ||
|
|
||
| const serverData: Omit<MCPServerConfig, 'id'> = { | ||
| name, | ||
| type: serverType, | ||
| description: (serverConfig.description as string) || undefined, | ||
| enabled: serverConfig.enabled !== false, | ||
| }; | ||
| const serverData: Omit<MCPServerConfig, 'id'> = { | ||
| name, | ||
| type: serverType, | ||
| description: (serverConfig.description as string) || undefined, | ||
| enabled: serverConfig.enabled !== false, | ||
| }; | ||
|
|
||
| if (serverType === 'stdio') { | ||
| serverData.command = serverConfig.command as string; | ||
| if (Array.isArray(serverConfig.args)) { | ||
| serverData.args = serverConfig.args as string[]; | ||
| } | ||
| if (typeof serverConfig.env === 'object' && serverConfig.env !== null) { | ||
| serverData.env = serverConfig.env as Record<string, string>; | ||
| } | ||
| } else { | ||
| serverData.url = serverConfig.url as string; | ||
| if (typeof serverConfig.headers === 'object' && serverConfig.headers !== null) { | ||
| serverData.headers = serverConfig.headers as Record<string, string>; | ||
| } | ||
| if (serverType === 'stdio') { | ||
| serverData.command = serverConfig.command as string; | ||
| if (Array.isArray(serverConfig.args)) { | ||
| serverData.args = serverConfig.args as string[]; | ||
| } | ||
| if (typeof serverConfig.env === 'object' && serverConfig.env !== null) { | ||
| serverData.env = serverConfig.env as Record<string, string>; | ||
| } | ||
| } else { | ||
| serverData.url = serverConfig.url as string; | ||
| if (typeof serverConfig.headers === 'object' && serverConfig.headers !== null) { | ||
| serverData.headers = serverConfig.headers as Record<string, string>; | ||
| } | ||
| } | ||
|
|
||
| const existing = existingByName.get(name); | ||
| if (existing) { | ||
| updateMCPServer(existing.id, serverData); | ||
| } else { | ||
| addMCPServer(serverData); | ||
| // Try to match by ID first, then by name | ||
| const existing = id ? existingById.get(id) : existingByName.get(name); | ||
| if (existing) { | ||
| updateMCPServer(existing.id, serverData); | ||
| processedIds.add(existing.id); | ||
| } else { | ||
| addMCPServer(serverData); | ||
| } | ||
| } | ||
|
|
||
| // Remove servers that are no longer in the JSON | ||
| for (const server of mcpServers) { | ||
| if (!processedIds.has(server.id)) { | ||
| removeMCPServer(server.id); | ||
| } | ||
| } | ||
|
Comment on lines
+739
to
+754
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π§© Analysis chainπ Script executed: #!/bin/bash
# First, get the file and check its size
wc -l "apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts"Repository: AutoMaker-Org/automaker Length of output: 150 π Script executed: #!/bin/bash
# View the specific function mentioned in the review (lines 675-755+)
sed -n '675,760p' "apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts" | cat -nRepository: AutoMaker-Org/automaker Length of output: 3979 π Script executed: #!/bin/bash
# Find where addMCPServer is defined/called
rg "addMCPServer\s*=" "apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts" -A 5Repository: AutoMaker-Org/automaker Length of output: 49 π Script executed: #!/bin/bash
# Check how mcpServers state is declared and updated
rg "mcpServers" "apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts" -B 2 -A 2 | head -100Repository: AutoMaker-Org/automaker Length of output: 2958 π Script executed: #!/bin/bash
# Find the hook or context providing these functions
rg "addMCPServer.*function|addMCPServer.*=.*\(" "apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts" -B 3 -A 10 | head -80Repository: AutoMaker-Org/automaker Length of output: 49 π Script executed: #!/bin/bash
# Search for where these functions are returned from the hook
sed -n '800,891p' "apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts" | cat -nRepository: AutoMaker-Org/automaker Length of output: 2906 π Script executed: #!/bin/bash
# Look for the functions being called within the component - are they state setters or external mutations?
rg "const.*MCPServer.*useContext|const.*MCPServer.*useState|const.*MCPServer.*use" "apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts" -A 2Repository: AutoMaker-Org/automaker Length of output: 49 π Script executed: #!/bin/bash
# Search for where addMCPServer, updateMCPServer, removeMCPServer are actually defined
rg "const addMCPServer|const updateMCPServer|const removeMCPServer" "apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts" -B 2 -A 8Repository: AutoMaker-Org/automaker Length of output: 49 π Script executed: #!/bin/bash
# Look at the beginning of the hook to find where these come from
head -100 "apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts" | cat -nRepository: AutoMaker-Org/automaker Length of output: 4568 π Script executed: #!/bin/bash
# Find the app store implementation
find . -name "app-store.*" -type f | head -5Repository: AutoMaker-Org/automaker Length of output: 99 π Script executed: #!/bin/bash
# Search for addMCPServer in the app store
rg "addMCPServer" --type ts --type tsx -B 3 -A 10 | grep -A 10 "addMCPServer.*=" | head -60Repository: AutoMaker-Org/automaker Length of output: 94 π Script executed: #!/bin/bash
# Search for addMCPServer in the app store
rg "addMCPServer" "apps/ui/src/store/app-store.ts" -B 3 -A 10Repository: AutoMaker-Org/automaker Length of output: 996 π Script executed: #!/bin/bash
# Get the app store file size and view relevant sections
wc -l "apps/ui/src/store/app-store.ts"Repository: AutoMaker-Org/automaker Length of output: 102 π Script executed: #!/bin/bash
# View the removeMCPServer implementation to understand state updates
rg "removeMCPServer" "apps/ui/src/store/app-store.ts" -A 5 | head -20Repository: AutoMaker-Org/automaker Length of output: 537 π Script executed: #!/bin/bash
# Verify whether addMCPServer is synchronous by checking if it returns anything and how state updates work in this store
sed -n '1,50p' "apps/ui/src/store/app-store.ts" | cat -nRepository: AutoMaker-Org/automaker Length of output: 1284 π Script executed: #!/bin/bash
# Let me verify the exact issue by tracing through the logic
# Check if addMCPServer generates an ID and returns it
sed -n '675,800p' "apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts" | grep -A 50 "handleSaveGlobalJsonArray"Repository: AutoMaker-Org/automaker Length of output: 2110 π Script executed: #!/bin/bash
# Check if there's any missing code that should track the new server ID
rg "addMCPServer" "apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts" -B 5 -A 5Repository: AutoMaker-Org/automaker Length of output: 1679 π Script executed: #!/bin/bash
# Verify the Zustand store behavior - check if set() is synchronous
sed -n '1,100p' "apps/ui/src/store/app-store.ts" | grep -A 3 "create("Repository: AutoMaker-Org/automaker Length of output: 49 π Script executed: #!/bin/bash
# Double-check the exact sequence in handleSaveGlobalJsonArray
# Extract lines 675-755 to see the full function logic
sed -n '675,755p' "apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts" | cat -nRepository: AutoMaker-Org/automaker Length of output: 3700 π Script executed: #!/bin/bash
# Verify Zustand's set() synchronicity and how it affects state
grep -A 20 "create(" "apps/ui/src/store/app-store.ts" | head -30Repository: AutoMaker-Org/automaker Length of output: 49 π Script executed: #!/bin/bash
# Check if there's any await or callback used with addMCPServer in the function
sed -n '700,760p' "apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts" | grep -E "addMCPServer|await|then"Repository: AutoMaker-Org/automaker Length of output: 148 π Script executed: #!/bin/bash
# Verify the full context - is there any awaiting or syncing after the loop?
sed -n '740,780p' "apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts" | cat -nRepository: AutoMaker-Org/automaker Length of output: 1852 π Script executed: #!/bin/bash
# Check the full function signature and whether handleSaveGlobalJsonArray gets called
rg "handleSaveGlobalJsonArray" "apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts" -B 2 -A 2Repository: AutoMaker-Org/automaker Length of output: 519 π Script executed: #!/bin/bash
# Verify by looking at the handleSaveGlobalJsonEdit function to see how handleSaveGlobalJsonArray is invoked
sed -n '600,750p' "apps/ui/src/components/views/settings-view/mcp-servers/hooks/use-mcp-servers.ts" | grep -B 30 "handleSaveGlobalJsonArray"Repository: AutoMaker-Org/automaker Length of output: 1912 Critical: Newly added servers are not tracked in When The fix requires either: (1) having π€ Prompt for AI Agents |
||
| }; | ||
|
|
||
| // Helper: Process object format (legacy Claude Desktop format) | ||
| const handleSaveGlobalJsonObject = async (servers: Record<string, unknown>) => { | ||
| // Validate all servers first | ||
| for (const [name, config] of Object.entries(servers)) { | ||
| if (typeof config !== 'object' || config === null) { | ||
| toast.error(`Invalid config for "${name}"`); | ||
| throw new Error('Invalid config'); | ||
| } | ||
|
|
||
| const serverConfig = config as Record<string, unknown>; | ||
| const serverType = (serverConfig.type as string) || 'stdio'; | ||
|
|
||
| if (serverType === 'stdio') { | ||
| if (!serverConfig.command || typeof serverConfig.command !== 'string') { | ||
| toast.error(`Command is required for "${name}" (stdio)`); | ||
| throw new Error('Command required'); | ||
| } | ||
| } else if (serverType === 'sse' || serverType === 'http') { | ||
| if (!serverConfig.url || typeof serverConfig.url !== 'string') { | ||
| toast.error(`URL is required for "${name}" (${serverType})`); | ||
| throw new Error('URL required'); | ||
| } | ||
| processedNames.add(name); | ||
| } | ||
| } | ||
|
|
||
| // Create a map of existing servers by name for updating | ||
| const existingByName = new Map(mcpServers.map((s) => [s.name, s])); | ||
| const processedNames = new Set<string>(); | ||
|
|
||
| // Remove servers that are no longer in the JSON | ||
| for (const server of mcpServers) { | ||
| if (!processedNames.has(server.name)) { | ||
| removeMCPServer(server.id); | ||
| // Update or add servers | ||
| for (const [name, config] of Object.entries(servers)) { | ||
| const serverConfig = config as Record<string, unknown>; | ||
| const serverType = (serverConfig.type as ServerType) || 'stdio'; | ||
|
|
||
| const serverData: Omit<MCPServerConfig, 'id'> = { | ||
| name, | ||
| type: serverType, | ||
| description: (serverConfig.description as string) || undefined, | ||
| enabled: serverConfig.enabled !== false, | ||
| }; | ||
|
|
||
| if (serverType === 'stdio') { | ||
| serverData.command = serverConfig.command as string; | ||
| if (Array.isArray(serverConfig.args)) { | ||
| serverData.args = serverConfig.args as string[]; | ||
| } | ||
| if (typeof serverConfig.env === 'object' && serverConfig.env !== null) { | ||
| serverData.env = serverConfig.env as Record<string, string>; | ||
| } | ||
| } else { | ||
| serverData.url = serverConfig.url as string; | ||
| if (typeof serverConfig.headers === 'object' && serverConfig.headers !== null) { | ||
| serverData.headers = serverConfig.headers as Record<string, string>; | ||
| } | ||
| } | ||
|
|
||
| await syncSettingsToServer(); | ||
| const existing = existingByName.get(name); | ||
| if (existing) { | ||
| updateMCPServer(existing.id, serverData); | ||
| } else { | ||
| addMCPServer(serverData); | ||
| } | ||
| processedNames.add(name); | ||
| } | ||
|
|
||
| toast.success('MCP servers configuration updated'); | ||
| setIsGlobalJsonEditOpen(false); | ||
| setGlobalJsonValue(''); | ||
| } catch (error) { | ||
| toast.error('Invalid JSON: ' + (error instanceof Error ? error.message : 'Parse error')); | ||
| // Remove servers that are no longer in the JSON | ||
| for (const server of mcpServers) { | ||
| if (!processedNames.has(server.name)) { | ||
| removeMCPServer(server.id); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the new
handleSaveGlobalJsonEditfunction, the helper methods (handleSaveGlobalJsonArrayandhandleSaveGlobalJsonObject) display atoastfor validation errors and thenthrowan error. Thecatchblock in this function then catches that error and displays a second, more generic toast. This results in two error toasts being shown to the user for a single validation failure, which can be confusing.To improve the user experience, the helper functions should return a boolean indicating success or failure instead of throwing an error for validation issues. This allows the main function to gracefully stop execution without triggering the
catchblock, ensuring only one clear error message is displayed.