Skip to content

Commit 54ae70c

Browse files
author
jzhu
committed
fix copilot mcp formaat
1 parent 6adf05b commit 54ae70c

2 files changed

Lines changed: 317 additions & 8 deletions

File tree

code_assistant_manager/mcp/base_client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -986,8 +986,9 @@ def _get_config_locations(self, tool_name: str):
986986
Path.cwd() / ".gemini" / "settings.json",
987987
],
988988
"copilot": [
989-
home / ".config" / "GitHub" / "Copilot" / "mcp.json",
989+
home / ".copilot" / "mcp-config.json",
990990
home / ".copilot" / "mcp.json",
991+
home / ".config" / "GitHub" / "Copilot" / "mcp.json",
991992
],
992993
"codex": [
993994
home / ".config" / "GitHub" / "Codex" / "mcp.json",
Lines changed: 315 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
"""Copilot MCP client implementation."""
22

3+
import json
4+
from pathlib import Path
5+
from typing import Dict
36
from .base_client import MCPClient
7+
from .base import print_squared_frame
48

59

610
class CopilotMCPClient(MCPClient):
@@ -9,15 +13,319 @@ class CopilotMCPClient(MCPClient):
913
def __init__(self):
1014
super().__init__("copilot")
1115

16+
def is_server_installed(self, tool_name: str, server_name: str) -> bool:
17+
"""Check if a server is installed by reading Copilot mcp-config.json files."""
18+
config_locations = self._get_config_locations(tool_name)
19+
for config_path in config_locations:
20+
if config_path.exists():
21+
try:
22+
with open(config_path, 'r') as f:
23+
config = json.load(f)
24+
25+
# Check for mcpServers in Copilot config
26+
if "mcpServers" in config and isinstance(config["mcpServers"], dict):
27+
if server_name in config["mcpServers"]:
28+
return True
29+
except Exception:
30+
continue
31+
return False
32+
33+
def add_server(self, server_name: str, scope: str = "user") -> bool:
34+
"""Add a server by directly editing Copilot mcp-config.json files based on scope."""
35+
return self._fallback_add_server_scoped(server_name, scope)
36+
37+
def _fallback_add_server_scoped(self, server_name: str, scope: str) -> bool:
38+
"""Add a server to Copilot config files based on scope."""
39+
# Get server configuration - try registry first, then legacy
40+
server_info = self.get_server_config(server_name)
41+
if not server_info:
42+
print(f" No server configuration found for {server_name}")
43+
return False
44+
45+
# Convert to Copilot format
46+
copilot_server_info = self._convert_to_copilot_format(server_info)
47+
48+
# Determine target locations based on scope
49+
config_locations = self._get_config_locations(self.tool_name)
50+
if scope == "user":
51+
target_locations = [config_locations[0]] # User-level only
52+
elif scope == "project":
53+
# Project shared and personal (skip user)
54+
target_locations = config_locations[1:]
55+
else:
56+
target_locations = config_locations
57+
58+
for config_path in target_locations:
59+
if self._add_server_to_config(config_path, server_name, copilot_server_info):
60+
level = "user-level" if config_path == config_locations[0] else "project-level"
61+
print(f" Added {server_name} to {level} configuration")
62+
return True
63+
64+
return False
65+
66+
def _convert_server_config_to_client_format(self, server_config) -> Dict:
67+
"""
68+
Override to provide Copilot-specific server config conversion.
69+
70+
Args:
71+
server_config: STDIOServerConfig or RemoteServerConfig object
72+
73+
Returns:
74+
Dict in Copilot configuration format
75+
"""
76+
if hasattr(server_config, 'url') and server_config.url:
77+
# Remote server (shouldn't happen for memory server, but handle it)
78+
config = {
79+
"type": "http",
80+
"url": server_config.url,
81+
}
82+
if hasattr(server_config, 'headers') and server_config.headers:
83+
config["headers"] = server_config.headers
84+
return config
85+
else:
86+
# STDIO server - convert to Copilot format
87+
# Get the basic server info first
88+
server_info = {
89+
"command": getattr(server_config, 'command', 'echo'),
90+
"args": getattr(server_config, 'args', []),
91+
"env": getattr(server_config, 'env', {}),
92+
}
93+
94+
# Use the Copilot-specific conversion
95+
result = self._convert_to_copilot_format(server_info)
96+
return result
97+
98+
def remove_server(self, server_name: str, scope: str = "user") -> bool:
99+
"""Remove a server by directly editing Copilot mcp-config.json files based on scope."""
100+
config_locations = self._get_config_locations(self.tool_name)
101+
102+
if scope == "user":
103+
target_locations = [config_locations[0]] # User-level only
104+
elif scope == "project":
105+
# Project shared and personal (skip user)
106+
target_locations = config_locations[1:]
107+
else:
108+
target_locations = config_locations
109+
110+
success = False
111+
for config_path in target_locations:
112+
if self._remove_server_from_config(config_path, server_name):
113+
level = "user-level" if config_path == config_locations[0] else "project-level"
114+
print(f" Removed {server_name} from {level} configuration")
115+
success = True
116+
break # Remove from first found location
117+
118+
if not success:
119+
level = "user-level" if scope == "user" else "project-level" if scope == "project" else "configuration"
120+
print(f" {server_name} not found in {level} configuration")
121+
122+
return success
123+
124+
def add_all_servers(self, scope: str = "user") -> bool:
125+
"""Add all MCP servers for this tool based on scope."""
126+
tool_configs = self.get_tool_config(self.tool_name, scope)
127+
if not tool_configs:
128+
print_squared_frame(f"{self.tool_name.upper()} MCP SERVERS", f"No MCP server configurations found for {self.tool_name}")
129+
return False
130+
131+
# Print initial frame for adding operation
132+
print_squared_frame(f"{self.tool_name.upper()} MCP SERVERS", f"Adding MCP servers for {self.tool_name}...")
133+
134+
if scope == "user":
135+
target_locations = [self._get_config_locations(self.tool_name)[0]] # User-level only
136+
elif scope == "project":
137+
target_locations = self._get_config_locations(self.tool_name)[1:] # Project only
138+
else:
139+
target_locations = self._get_config_locations(self.tool_name)
140+
141+
success_count = 0
142+
for server_name in tool_configs.keys():
143+
server_info = self.get_server_config(server_name)
144+
if not server_info:
145+
print(f" No server configuration found for {server_name}")
146+
continue
147+
148+
# Convert to Copilot format
149+
copilot_server_info = self._convert_to_copilot_format(server_info)
150+
151+
added = False
152+
for config_path in target_locations:
153+
if self._add_server_to_config(config_path, server_name, copilot_server_info):
154+
level = "user-level" if config_path == target_locations[0] else "project-level"
155+
print(f" Added {server_name} to {level} configuration")
156+
added = True
157+
success_count += 1
158+
break # Add to first available location
159+
if not added:
160+
print(f" ✗ Failed to add {server_name}")
161+
162+
# Print success frame
163+
if success_count > 0:
164+
print_squared_frame(f"{self.tool_name.upper()} MCP SERVERS", f"✓ Successfully added {success_count} MCP servers for {self.tool_name}")
165+
else:
166+
print_squared_frame(f"{self.tool_name.upper()} MCP SERVERS", f"✗ Failed to add any MCP servers for {self.tool_name}")
167+
168+
return success_count > 0
169+
170+
def _convert_to_copilot_format(self, server_info: dict) -> dict:
171+
"""Convert global server config to Copilot mcpServers format."""
172+
# Start with the stdio format conversion
173+
stdio_format = self._convert_server_to_stdio_format(server_info)
174+
175+
# Convert Copilot-specific format
176+
copilot_format = {
177+
"type": "local", # Copilot uses "local" instead of "stdio"
178+
"command": stdio_format.get("command", "echo"),
179+
"args": stdio_format.get("args", []),
180+
"tools": ["*"] # Copilot requires tools specification
181+
}
182+
183+
# Include env if present
184+
if "env" in stdio_format and stdio_format["env"]:
185+
copilot_format["env"] = stdio_format["env"]
186+
187+
return copilot_format
188+
189+
def list_servers(self, scope: str = "all") -> bool:
190+
"""List servers by reading Copilot mcp-config.json files."""
191+
tool_configs = self.get_tool_config(self.tool_name)
192+
if not tool_configs:
193+
print(f"No MCP server configurations found for {self.tool_name}")
194+
return False
195+
196+
config_locations = self._get_config_locations(self.tool_name)
197+
user_servers = {}
198+
project_servers = {}
199+
200+
for i, config_path in enumerate(config_locations):
201+
if config_path.exists():
202+
try:
203+
with open(config_path, 'r') as f:
204+
config = json.load(f)
205+
206+
if "mcpServers" in config and isinstance(config["mcpServers"], dict):
207+
if i == 0: # user-level
208+
user_servers.update(config["mcpServers"])
209+
else: # project-level
210+
project_servers.update(config["mcpServers"])
211+
212+
except Exception as e:
213+
print(f"Warning: Failed to read {config_path}: {e}")
214+
continue
215+
216+
content_lines = []
217+
218+
show_user = scope in ["all", "user"]
219+
show_project = scope in ["all", "project"]
220+
221+
if show_user and user_servers:
222+
content_lines.append("User-level servers:")
223+
for name, config in user_servers.items():
224+
content_lines.append(f" {name}: {config}")
225+
if show_project and project_servers:
226+
content_lines.append("")
227+
228+
if show_project and project_servers:
229+
content_lines.append("Project-level servers:")
230+
for name, config in project_servers.items():
231+
content_lines.append(f" {name}: {config}")
232+
233+
servers_to_show = (show_user and user_servers) or (show_project and project_servers)
234+
235+
if servers_to_show:
236+
content = "\n".join(content_lines)
237+
print_squared_frame(f"{self.tool_name.upper()} MCP SERVERS", content)
238+
return True
239+
else:
240+
level_desc = "" if scope == "all" else "user-level" if scope == "user" else "project-level" if scope == "project" else "configuration"
241+
if level_desc:
242+
content = f"No MCP servers configured in {level_desc} configuration"
243+
else:
244+
content = "No MCP servers configured"
245+
print_squared_frame(f"{self.tool_name.upper()} MCP SERVERS", content)
246+
return True
247+
12248
def _get_config_locations(self, tool_name: str):
13249
"""Override to provide Copilot-specific config locations."""
14-
locations = super()._get_config_locations(tool_name)
15-
# Add Copilot-specific locations at the beginning
16-
from pathlib import Path
17250
home = Path.home()
18-
copilot_locations = [
19-
home / ".copilot" / "mcp.json",
20-
home / ".config" / "GitHub" / "Copilot" / "mcp.json",
251+
# Copilot uses mcp-config.json at user level only
252+
locations = [
253+
home / ".copilot" / "mcp-config.json", # User-level (primary)
254+
Path.cwd() / ".mcp.json", # Project-level
21255
]
22-
locations[:0] = copilot_locations # Add to beginning
23256
return locations
257+
258+
def _get_config_paths(self, scope: str):
259+
"""Override to provide Copilot-specific config paths for scope-based operations."""
260+
home = Path.home()
261+
if scope == "user":
262+
return [home / ".copilot" / "mcp-config.json"]
263+
elif scope == "project":
264+
return [Path.cwd() / ".mcp.json"]
265+
else: # all
266+
return [
267+
home / ".copilot" / "mcp-config.json",
268+
Path.cwd() / ".mcp.json"
269+
]
270+
271+
def _add_server_config_to_file(self, config_path, server_name: str, client_config: dict) -> bool:
272+
"""Add server config to a Copilot JSON file."""
273+
config_path = Path(config_path)
274+
275+
try:
276+
# Load existing config
277+
config = {}
278+
if config_path.exists():
279+
with open(config_path, 'r') as f:
280+
config = json.load(f)
281+
282+
# Ensure mcpServers exists
283+
if "mcpServers" not in config:
284+
config["mcpServers"] = {}
285+
286+
# Add the server config
287+
config["mcpServers"][server_name] = client_config
288+
289+
# Write back
290+
config_path.parent.mkdir(parents=True, exist_ok=True)
291+
with open(config_path, 'w') as f:
292+
json.dump(config, f, indent=2)
293+
294+
return True
295+
296+
except Exception as e:
297+
print(f"Error adding server to Copilot config {config_path}: {e}")
298+
return False
299+
300+
def _add_server_to_config(self, config_path, server_name: str, server_info: dict) -> bool:
301+
"""Add a server to a Copilot config file."""
302+
# Convert to Copilot format first
303+
copilot_server_info = self._convert_to_copilot_format(server_info)
304+
return self._add_server_config_to_file(config_path, server_name, copilot_server_info)
305+
306+
def _remove_server_from_config(self, config_path, server_name: str) -> bool:
307+
"""Remove a server from a Copilot config file."""
308+
config_path = Path(config_path)
309+
310+
try:
311+
if not config_path.exists():
312+
return False
313+
314+
with open(config_path, 'r') as f:
315+
config = json.load(f)
316+
317+
# Check if the server exists in mcpServers
318+
if "mcpServers" in config and isinstance(config["mcpServers"], dict):
319+
if server_name in config["mcpServers"]:
320+
del config["mcpServers"][server_name]
321+
322+
# Write back
323+
with open(config_path, 'w') as f:
324+
json.dump(config, f, indent=2)
325+
return True
326+
327+
return False
328+
329+
except Exception as e:
330+
print(f"Error removing server from Copilot config {config_path}: {e}")
331+
return False

0 commit comments

Comments
 (0)