@@ -25,7 +25,15 @@ def is_server_installed(self, tool_name: str, server_name: str) -> bool:
2525 # Check for mcp section in OpenCode config
2626 if "mcp" in config and isinstance (config ["mcp" ], dict ):
2727 if server_name in config ["mcp" ]:
28- return True
28+ server_config = config ["mcp" ][server_name ]
29+ # Check if it's in the correct OpenCode format
30+ # Should have "type": "local" and "command" as array
31+ if (isinstance (server_config , dict ) and
32+ server_config .get ("type" ) == "local" and
33+ isinstance (server_config .get ("command" ), list )):
34+ return True
35+ # If it's in the old format, consider it not properly installed
36+ # so it will be re-installed with the correct format
2937 except Exception :
3038 continue
3139 return False
@@ -49,6 +57,9 @@ def _add_server_config_to_file(
4957 content = config_path .read_text (encoding = "utf-8" ).strip ()
5058 config = json .loads (content ) if content else {}
5159
60+ # Normalize existing config to handle legacy formats
61+ config = self ._normalize_opencode_config (config )
62+
5263 # OpenCode uses "mcp" section
5364 if "mcp" not in config :
5465 config ["mcp" ] = {}
@@ -73,15 +84,22 @@ def _get_config_locations(self, tool_name: str):
7384 return [home / ".config" / "opencode" / "opencode.json" ]
7485
7586 def _convert_to_opencode_format (self , server_info : dict ) -> dict :
76- """Convert global server config to OpenCode format."""
77- # OpenCode uses the same format as the global config
78- # but may need some adjustments for local vs remote
87+ """Convert global server config to OpenCode format.
88+
89+ OpenCode expects local MCP servers to have a 'command' field as an array
90+ containing both the command and its arguments, unlike the standard MCP
91+ format which separates command and args.
92+ """
7993 opencode_config = {}
8094
8195 if server_info .get ("command" ):
8296 # Local server
8397 opencode_config ["type" ] = "local"
84- opencode_config ["command" ] = server_info ["command" ]
98+ # OpenCode expects command as an array combining command and args
99+ command_list = [server_info ["command" ]]
100+ if "args" in server_info and server_info ["args" ]:
101+ command_list .extend (server_info ["args" ])
102+ opencode_config ["command" ] = command_list
85103 if "env" in server_info :
86104 opencode_config ["env" ] = server_info ["env" ]
87105 elif server_info .get ("url" ):
@@ -257,6 +275,128 @@ def _add_server_to_user_config(self, server_name: str) -> bool:
257275 user_config_path , server_name , opencode_server_info
258276 )
259277
278+ def _normalize_opencode_config (self , config : dict ) -> dict :
279+ """Normalize OpenCode configuration to handle legacy formats.
280+
281+ Converts old format with separate command/args to new array format.
282+ """
283+ if "mcp" not in config :
284+ return config
285+
286+ normalized_config = config .copy ()
287+ normalized_config ["mcp" ] = {}
288+
289+ for server_name , server_config in config ["mcp" ].items ():
290+ if isinstance (server_config , dict ):
291+ normalized_server = server_config .copy ()
292+
293+ # Handle legacy format: convert separate command/args to array
294+ if (server_config .get ("type" ) in ["stdio" , "local" ] and
295+ isinstance (server_config .get ("command" ), str ) and
296+ "args" in server_config ):
297+
298+ # Combine command and args into array
299+ command_list = [server_config ["command" ]]
300+ if server_config ["args" ]:
301+ command_list .extend (server_config ["args" ])
302+
303+ normalized_server ["command" ] = command_list
304+ normalized_server ["type" ] = "local" # Normalize type to "local"
305+ # Remove the old "args" field
306+ normalized_server .pop ("args" , None )
307+
308+ normalized_config ["mcp" ][server_name ] = normalized_server
309+ else :
310+ # Keep non-dict configs as-is
311+ normalized_config ["mcp" ][server_name ] = server_config
312+
313+ return normalized_config
314+
315+ def add_server_with_config (
316+ self , server_name : str , server_config , scope : str = "user"
317+ ) -> bool :
318+ """Add a server configuration for OpenCode with proper format conversion."""
319+ # Convert the server config to OpenCode format
320+ opencode_config = self ._convert_to_opencode_format_from_schema (server_config )
321+
322+ # Get the config file path
323+ config_paths = self ._get_config_paths (scope )
324+ if not config_paths :
325+ print (f"No config paths found for scope '{ scope } '" )
326+ return False
327+
328+ # Add to the first available config path
329+ for config_path in config_paths :
330+ if self ._add_server_config_to_file (config_path , server_name , opencode_config ):
331+ print (f"✓ Successfully added { server_name } to OpenCode configuration" )
332+ return True
333+
334+ print (f"✗ Failed to add { server_name } to any OpenCode configuration file" )
335+ return False
336+
337+ def _convert_to_opencode_format_from_schema (self , server_config ) -> dict :
338+ """Convert a ServerConfig object to OpenCode format."""
339+ opencode_config = {}
340+
341+ # Handle remote servers
342+ if hasattr (server_config , "url" ) and server_config .url :
343+ opencode_config ["type" ] = "remote"
344+ opencode_config ["url" ] = server_config .url
345+ if hasattr (server_config , "headers" ) and server_config .headers :
346+ opencode_config ["headers" ] = server_config .headers
347+ else :
348+ # Handle STDIO servers - OpenCode expects command as array
349+ opencode_config ["type" ] = "local"
350+ command = getattr (server_config , "command" , "echo" )
351+ args = getattr (server_config , "args" , [])
352+
353+ # Combine command and args into array for OpenCode
354+ if isinstance (command , str ):
355+ opencode_config ["command" ] = [command ] + args
356+ else :
357+ opencode_config ["command" ] = command + args
358+
359+ # Add environment variables if present
360+ env_vars = getattr (server_config , "env" , {})
361+ if env_vars :
362+ opencode_config ["env" ] = env_vars
363+
364+ # Add enabled flag
365+ opencode_config ["enabled" ] = True
366+
367+ return opencode_config
368+
369+ def add_server (self , server_name : str , scope : str = "user" ) -> bool :
370+ """Add a specific MCP server for OpenCode by directly managing the configuration."""
371+ # Get server configuration from the main config
372+ success , config = self .load_config ()
373+ if not success or "servers" not in config :
374+ print (f" No server configuration found for { server_name } " )
375+ return False
376+
377+ server_info = config ["servers" ].get (server_name )
378+ if not server_info :
379+ print (f" Server info not found for { server_name } " )
380+ return False
381+
382+ # Convert to OpenCode format
383+ opencode_server_info = self ._convert_to_opencode_format (server_info )
384+
385+ # Get the config file path
386+ config_paths = self ._get_config_paths (scope )
387+ if not config_paths :
388+ print (f"No config paths found for scope '{ scope } '" )
389+ return False
390+
391+ # Add to the first available config path
392+ for config_path in config_paths :
393+ if self ._add_server_config_to_file (config_path , server_name , opencode_server_info ):
394+ print (f"✓ Successfully added { server_name } to OpenCode configuration" )
395+ return True
396+
397+ print (f"✗ Failed to add { server_name } to any OpenCode configuration file" )
398+ return False
399+
260400 def list_servers (self , scope : str = "all" ) -> bool :
261401 """List servers by reading OpenCode config files."""
262402 tool_configs = self .get_tool_config (self .tool_name )
@@ -302,6 +442,61 @@ def list_servers(self, scope: str = "all") -> bool:
302442 print_squared_frame (f"{ self .tool_name .upper ()} MCP SERVERS" , content )
303443 return True
304444
445+ def normalize_config_file (self , config_path : Path = None ) -> bool :
446+ """Normalize an OpenCode configuration file to fix legacy formats.
447+
448+ This method can be used to fix existing configuration files that use
449+ the old command/args format to the new array format expected by OpenCode.
450+ """
451+ if config_path is None :
452+ home = Path .home ()
453+ config_path = home / ".config" / "opencode" / "opencode.json"
454+
455+ try :
456+ # Load existing config
457+ config = {}
458+ if config_path .exists ():
459+ content = config_path .read_text (encoding = "utf-8" ).strip ()
460+ config = json .loads (content ) if content else {}
461+ else :
462+ print (f"Config file not found: { config_path } " )
463+ return False
464+
465+ # Normalize the config
466+ normalized_config = self ._normalize_opencode_config (config )
467+
468+ # Check if any changes were made
469+ if config != normalized_config :
470+ # Write back the normalized config
471+ config_path .parent .mkdir (parents = True , exist_ok = True )
472+ with open (config_path , "w" , encoding = "utf-8" ) as f :
473+ json .dump (normalized_config , f , indent = 2 , ensure_ascii = False )
474+
475+ print (f"✓ Normalized OpenCode configuration at { config_path } " )
476+ return True
477+ else :
478+ print (f"Configuration at { config_path } is already in the correct format" )
479+ return True
480+
481+ except Exception as e :
482+ print (f"Error normalizing OpenCode config { config_path } : { e } " )
483+ return False
484+
485+ def _get_config_paths (self , scope : str ):
486+ """Get OpenCode configuration file paths based on scope."""
487+ from pathlib import Path
488+
489+ home = Path .home ()
490+ if scope == "user" :
491+ return [home / ".config" / "opencode" / "opencode.json" ]
492+ elif scope == "project" :
493+ return [Path .cwd () / ".opencode.json" ]
494+ else : # all
495+ return [
496+ home / ".config" / "opencode" / "opencode.json" ,
497+ Path .cwd () / ".opencode.json"
498+ ]
499+
305500 def remove_server (self , server_name : str , scope : str = "user" ) -> bool :
306501 """Remove a server from OpenCode config files based on scope."""
307502 config_locations = self ._get_config_locations (self .tool_name )
0 commit comments