Skip to content

Commit c031548

Browse files
committed
fix: ensure OpenCode MCP configurations use correct array format
- Update is_server_installed to validate correct OpenCode format - Override add_server_with_config to generate proper array-based command format - Add _convert_to_opencode_format_from_schema for ServerConfig objects - Implement _get_config_paths for proper configuration file handling - Add normalization support for legacy configurations OpenCode expects 'command' as an array containing both command and arguments, not separate 'command' string and 'args' array fields.
1 parent 06a2888 commit c031548

File tree

1 file changed

+200
-5
lines changed

1 file changed

+200
-5
lines changed

code_assistant_manager/mcp/opencode.py

Lines changed: 200 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)