auto-coder 0.1.287__py3-none-any.whl → 0.1.289__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of auto-coder might be problematic. Click here for more details.

@@ -2,16 +2,18 @@ import os
2
2
  import json
3
3
  import asyncio
4
4
  import aiohttp
5
- from datetime import datetime
5
+ from datetime import datetime, timedelta
6
6
  from typing import Dict, List, Optional, Any, Set, Optional
7
7
  from pathlib import Path
8
8
  from pydantic import BaseModel, Field
9
9
 
10
10
  from mcp import ClientSession
11
11
  from mcp.client.stdio import stdio_client, StdioServerParameters
12
+ from mcp.client.sse import sse_client
12
13
  import mcp.types as mcp_types
13
14
  from loguru import logger
14
- import time
15
+ from contextlib import AsyncExitStack
16
+ from datetime import timedelta
15
17
 
16
18
 
17
19
  class McpTool(BaseModel):
@@ -55,12 +57,9 @@ class McpServer(BaseModel):
55
57
  class McpConnection:
56
58
  """Represents an active MCP server connection"""
57
59
 
58
- def __init__(self, server: McpServer, session: ClientSession, transport_manager):
60
+ def __init__(self, server: McpServer, session: ClientSession):
59
61
  self.server = server
60
- self.session = session
61
- self.transport_manager = (
62
- transport_manager # Will hold transport context manager
63
- )
62
+ self.session = session
64
63
 
65
64
 
66
65
  MCP_PERPLEXITY_SERVER = '''
@@ -81,6 +80,7 @@ MCP_BUILD_IN_SERVERS = {
81
80
  "perplexity": json.loads(MCP_PERPLEXITY_SERVER)["perplexity"]
82
81
  }
83
82
 
83
+
84
84
  class McpHub:
85
85
  """
86
86
  Manages MCP server connections and interactions.
@@ -105,6 +105,7 @@ class McpHub:
105
105
  self.settings_path = Path(settings_path)
106
106
  self.connections: Dict[str, McpConnection] = {}
107
107
  self.is_connecting = False
108
+ self.exit_stacks: Dict[str, AsyncExitStack] = {}
108
109
 
109
110
  # Ensure settings directory exists
110
111
  self.settings_path.parent.mkdir(parents=True, exist_ok=True)
@@ -116,26 +117,31 @@ class McpHub:
116
117
  def _write_default_settings(self):
117
118
  """Write default MCP settings file"""
118
119
  default_settings = {"mcpServers": {}}
119
- with open(self.settings_path, "w",encoding="utf-8") as f:
120
+ with open(self.settings_path, "w", encoding="utf-8") as f:
120
121
  json.dump(default_settings, f, indent=2)
121
122
 
122
- async def add_server_config(self, name: str, config:Dict[str,Any]) -> None:
123
+ async def add_server_config(self, name: str, config: Dict[str, Any]) -> None:
123
124
  """
124
125
  Add or update MCP server configuration in settings file.
125
126
 
126
127
  Args:
127
128
  server_name_or_config: Name of the server or server configuration dictionary
128
129
  """
129
- try:
130
- settings = self._read_settings()
131
- settings["mcpServers"][name] = config
132
- with open(self.settings_path, "w",encoding="utf-8") as f:
133
- json.dump(settings, f, indent=2, ensure_ascii=False)
134
- await self.initialize()
135
- logger.info(f"Added/updated MCP server config: {name}")
136
- except Exception as e:
137
- logger.error(f"Failed to add MCP server config: {e}")
138
- raise
130
+ settings = self._read_settings()
131
+ settings["mcpServers"][name] = config
132
+ with open(self.settings_path, "w", encoding="utf-8") as f:
133
+ json.dump(settings, f, indent=2, ensure_ascii=False)
134
+ await self.initialize()
135
+ if name in self.connections:
136
+ if self.connections[name].server.status == "connected":
137
+ logger.info(f"Added/updated MCP server config: {name}")
138
+ return True
139
+ else:
140
+ logger.error(f"Failed to add MCP server config: {name}")
141
+ return False
142
+ else:
143
+ logger.error(f"Failed to add MCP server config: {name}")
144
+ return False
139
145
 
140
146
  async def remove_server_config(self, name: str) -> None:
141
147
  """
@@ -148,7 +154,7 @@ class McpHub:
148
154
  settings = self._read_settings()
149
155
  if name in settings["mcpServers"]:
150
156
  del settings["mcpServers"][name]
151
- with open(self.settings_path, "w",encoding="utf-8") as f:
157
+ with open(self.settings_path, "w", encoding="utf-8") as f:
152
158
  json.dump(settings, f, indent=2, ensure_ascii=False)
153
159
  logger.info(f"Removed MCP server config: {name}")
154
160
  await self.initialize()
@@ -160,18 +166,14 @@ class McpHub:
160
166
 
161
167
  async def initialize(self):
162
168
  """Initialize MCP server connections from settings"""
163
- try:
164
- config = self._read_settings()
165
- await self.update_server_connections(config.get("mcpServers", {}))
166
- except Exception as e:
167
- logger.error(f"Failed to initialize MCP servers: {e}")
168
- raise
169
+ config = self._read_settings()
170
+ await self.update_server_connections(config.get("mcpServers", {}))
169
171
 
170
172
  def get_servers(self) -> List[McpServer]:
171
173
  """Get list of all configured servers"""
172
174
  return [conn.server for conn in self.connections.values()]
173
175
 
174
- async def connect_to_server(self, name: str, config: dict) -> None:
176
+ async def connect_to_server(self, name: str, config: dict) -> McpServer:
175
177
  """
176
178
  Establish connection to an MCP server with proper resource management
177
179
  """
@@ -179,79 +181,90 @@ class McpHub:
179
181
  if name in self.connections:
180
182
  await self.delete_connection(name)
181
183
 
182
- try:
183
- server = McpServer(
184
- name=name, config=json.dumps(config), status="connecting"
185
- )
184
+ self.exit_stacks[name] = AsyncExitStack()
185
+ exit_stack = self.exit_stacks[name]
186
+
187
+ server = McpServer(
188
+ name=name, config=json.dumps(config), status="connecting"
189
+ )
190
+ transport_config = config.get("transport", {
191
+ "type": "stdio",
192
+ "endpoint":""
193
+ })
186
194
 
195
+ if transport_config["type"] not in ["stdio", "sse"]:
196
+ raise ValueError(f"Invalid transport type: {transport_config['type']}")
197
+
198
+ if transport_config["type"] == "stdio":
187
199
  # Setup transport parameters
188
200
  server_params = StdioServerParameters(
189
201
  command=config["command"],
190
202
  args=config.get("args", []),
191
203
  env={**config.get("env", {}),
192
- "PATH": os.environ.get("PATH", "")},
204
+ "PATH": os.environ.get("PATH", "")},
193
205
  )
194
206
 
195
- # Create transport using context manager
196
- transport_manager = stdio_client(server_params)
197
- transport = await transport_manager.__aenter__()
198
- try:
199
- session = await ClientSession(transport[0], transport[1]).__aenter__()
200
- await session.initialize()
201
-
202
- # Store connection with transport manager
203
- connection = McpConnection(server, session, transport_manager)
204
- self.connections[name] = connection
205
-
206
- # Update server status and fetch capabilities
207
- server.status = "connected"
208
- server.tools = await self._fetch_tools(name)
209
- server.resources = await self._fetch_resources(name)
210
- server.resource_templates = await self._fetch_resource_templates(name)
211
-
212
- except Exception as e:
213
- # Clean up transport if session initialization fails
214
-
215
- await transport_manager.__aexit__(None, None, None)
216
- raise
217
-
207
+ # Create transport using context manager
208
+ transport = await exit_stack.enter_async_context(stdio_client(server_params))
209
+
210
+ if transport_config["type"] == "sse":
211
+ url = transport_config["endpoint"]
212
+ transport = await exit_stack.enter_async_context(sse_client(url))
213
+
214
+
215
+ try:
216
+ stdio, write = transport
217
+ session = await exit_stack.enter_async_context(
218
+ ClientSession(stdio, write, read_timeout_seconds=timedelta(seconds=60)))
219
+ await session.initialize()
220
+ connection = McpConnection(server, session)
221
+ self.connections[name] = connection
222
+ # Update server status and fetch capabilities
223
+ server.status = "connected"
224
+ server.tools = await self._fetch_tools(name)
225
+ server.resources = await self._fetch_resources(name)
226
+ server.resource_templates = await self._fetch_resource_templates(name)
227
+ return server
228
+
218
229
  except Exception as e:
219
230
  error_msg = str(e)
220
231
  logger.error(f"Failed to connect to server {name}: {error_msg}")
221
232
  if name in self.connections:
222
233
  self.connections[name].server.status = "disconnected"
223
234
  self.connections[name].server.error = error_msg
224
- raise
235
+ return server
236
+
237
+
238
+
239
+
240
+
225
241
 
226
242
  async def delete_connection(self, name: str) -> None:
227
243
  """
228
244
  Close and remove a server connection with proper cleanup
229
245
  """
230
246
  if name in self.connections:
231
- try:
232
- connection = self.connections[name]
233
- # Clean up in reverse order of creation
234
- await connection.session.__aexit__(None, None, None)
235
- await connection.transport_manager.__aexit__(None, None, None)
247
+
248
+ try:
236
249
  del self.connections[name]
237
250
  except Exception as e:
238
251
  logger.error(f"Error closing connection to {name}: {e}")
239
- # Continue with deletion even if cleanup fails
240
- if name in self.connections:
241
- del self.connections[name]
252
+
253
+ try:
254
+ exit_stack = self.exit_stacks[name]
255
+ await exit_stack.aclose()
256
+ del self.exit_stacks[name]
257
+ except Exception as e:
258
+ logger.error(f"Error cleaning up to {name}: {e}")
259
+
242
260
 
243
261
  async def refresh_server_connection(self, name: str) -> None:
244
262
  """
245
263
  Refresh a server connection
246
264
  """
247
- try:
248
- config = self._read_settings()
249
- await self.delete_connection(name)
250
- await self.connect_to_server(name, config.get("mcpServers", {}).get(name, {}))
251
- except Exception as e:
252
- logger.error(f"Failed to refresh MCP server {name}: {e}")
253
- raise
254
-
265
+ config = self._read_settings()
266
+ await self.delete_connection(name)
267
+ await self.connect_to_server(name, config.get("mcpServers", {}).get(name, {}))
255
268
 
256
269
  async def update_server_connections(self, new_servers: Dict[str, Any]) -> None:
257
270
  """
@@ -273,13 +286,22 @@ class McpHub:
273
286
 
274
287
  if not current_conn:
275
288
  # New server
276
- await self.connect_to_server(name, config)
277
- logger.info(f"Connected to new MCP server: {name}")
289
+ server = await self.connect_to_server(name, config)
290
+ if server.status == "connected":
291
+ logger.info(f"Connected to new MCP server: {name}")
292
+ else:
293
+ logger.error(
294
+ f"Failed to connect to new MCP server: {name}")
295
+
278
296
  elif current_conn.server.config != json.dumps(config):
279
297
  # Updated configuration
280
- await self.connect_to_server(name, config)
281
- logger.info(
282
- f"Reconnected MCP server with updated config: {name}")
298
+ server = await self.connect_to_server(name, config)
299
+ if server.status == "connected":
300
+ logger.info(
301
+ f"Reconnected MCP server with updated config: {name}")
302
+ else:
303
+ logger.error(
304
+ f"Failed to reconnected MCP server with updated config: {name}")
283
305
 
284
306
  finally:
285
307
  self.is_connecting = False
@@ -13,6 +13,7 @@ from pydantic import BaseModel
13
13
  import sys
14
14
  from loguru import logger
15
15
  from autocoder.utils.llms import get_single_llm
16
+ from autocoder.chat_auto_coder_lang import get_message_with_format
16
17
 
17
18
  @dataclass
18
19
  class McpRequest:
@@ -157,55 +158,135 @@ class McpServer:
157
158
  print(f"We have already updated the server configuration in ~/.autocoder/mcp/settings.json.\n")
158
159
  print(f"After installation, you can restart the auto-coder.chat using the server.\033[0m\n")
159
160
 
161
+ def _deep_merge_dicts(self, dict1, dict2):
162
+ """
163
+ 深度合并两个字典,包括嵌套的字典
164
+ dict1是基础字典,dict2是优先字典(当有冲突时保留dict2的值)
165
+ """
166
+ result = dict1.copy()
167
+ for key, value in dict2.items():
168
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
169
+ # 如果两个字典都包含相同的键,并且两个值都是字典,则递归合并
170
+ result[key] = self._deep_merge_dicts(result[key], value)
171
+ else:
172
+ # 否则,使用dict2的值覆盖或添加到结果
173
+ result[key] = value
174
+ return result
175
+
176
+ def _parse_command_line_args(self, server_name_or_config: str) -> tuple[str, dict]:
177
+ """Parse command-line style arguments into name and config"""
178
+ name = ""
179
+ config = {}
180
+ args = server_name_or_config.strip().split()
181
+ i = 0
182
+ while i < len(args):
183
+ if args[i] == "--name" and i + 1 < len(args):
184
+ name = args[i + 1]
185
+ i += 2
186
+ elif args[i] == "--command" and i + 1 < len(args):
187
+ config["command"] = args[i + 1]
188
+ i += 2
189
+ elif args[i] == "--args":
190
+ config["args"] = []
191
+ i += 1
192
+ while i < len(args) and not args[i].startswith("--"):
193
+ config["args"].append(args[i])
194
+ i += 1
195
+ elif args[i] == "--env":
196
+ config["env"] = {}
197
+ i += 1
198
+ while i < len(args) and not args[i].startswith("--"):
199
+ if "=" in args[i]:
200
+ key, value = args[i].split("=", 1)
201
+ config["env"][key] = value
202
+ i += 1
203
+ else:
204
+ i += 1
205
+
206
+ template_config = {}
207
+
208
+ if name in MCP_BUILD_IN_SERVERS:
209
+ template_config = MCP_BUILD_IN_SERVERS[name]
210
+ else:
211
+ # 查找外部server
212
+ external_servers = get_mcp_external_servers()
213
+ for s in external_servers:
214
+ if s.name == name:
215
+ if s.runtime == "python":
216
+ self._install_python_package(name)
217
+ template_config = {
218
+ "command": "python",
219
+ "args": [
220
+ "-m", name.replace("-", "_")
221
+ ],
222
+ }
223
+ elif s.runtime == "node":
224
+ self._install_node_package(name)
225
+ template_config = {
226
+ "command": "npx",
227
+ "args": [
228
+ "-y",
229
+ "-g",
230
+ name
231
+ ]
232
+ }
233
+ break
234
+
235
+ # 深度合并两个配置,以用户配置为主
236
+ config = self._deep_merge_dicts(template_config, config)
237
+
238
+ if not config.get("args") and (name.startswith("@") or config.get("command") in ["npx", "npm"]):
239
+ config["args"] = ["-y", "-g", name]
240
+
241
+ ## 如果有模板,则无需再次安装,处理模板的时候会自动安装
242
+ if not template_config:
243
+ # Install package if needed
244
+ if name.startswith("@") or config.get("command") in ["npx", "npm"]:
245
+ self._install_node_package(name)
246
+ else:
247
+ self._install_python_package(name)
248
+
249
+ return name, config
250
+
251
+ def _parse_json_config(self, server_name_or_config: str) -> tuple[str, dict]:
252
+ """Parse JSON configuration into name and config"""
253
+ raw_config = json.loads(server_name_or_config)
254
+ # 用户给了一个完整的配置
255
+ if "mcpServers" in raw_config:
256
+ raw_config = raw_config["mcpServers"]
257
+
258
+ # 取第一个server 配置
259
+ config = list(raw_config.values())[0]
260
+ name = list(raw_config.keys())[0]
261
+ if name.startswith("@") or config["command"] in ["npx", "npm"]:
262
+ for item in config["args"]:
263
+ if name in item:
264
+ self._install_node_package(item)
265
+ else:
266
+ self._install_python_package(name)
267
+
268
+ return name, config
269
+
270
+
160
271
  async def _install_server(self, request: McpInstallRequest, hub: McpHub) -> McpResponse:
161
272
  """Install an MCP server with module dependency check"""
162
273
  name = ""
163
274
  config = {}
164
275
  try:
165
276
  server_name_or_config = request.server_name_or_config
166
- try:
167
- raw_config = json.loads(server_name_or_config)
168
- # 用户给了一个完整的配置
169
- if "mcpServers" in raw_config:
170
- raw_config = raw_config["mcpServers"]
171
-
172
- # 取第一个server 配置
173
- config = list(raw_config.values())[0]
174
- name = list(raw_config.keys())[0]
175
- if name.startswith("@") or config["command"] in ["npx","npm"]:
176
- for item in config["args"]:
177
- if name in item:
178
- self._install_node_package(item)
179
- else:
180
- self._install_python_package(name)
181
- except json.JSONDecodeError:
182
- name = server_name_or_config.strip()
183
- if name not in MCP_BUILD_IN_SERVERS:
184
- # 查找外部server
185
- external_servers = get_mcp_external_servers()
186
- for s in external_servers:
187
- if s.name == name:
188
- if s.runtime == "python":
189
- self._install_python_package(name)
190
- config = {
191
- "command": "python",
192
- "args": [
193
- "-m", name.replace("-", "_")
194
- ],
195
- }
196
- elif s.runtime == "node":
197
- self._install_node_package(name)
198
- config = {
199
- "command": "npx",
200
- "args": [
201
- "-y",
202
- "-g",
203
- name
204
- ]
205
- }
206
- break
207
- else:
208
- config = MCP_BUILD_IN_SERVERS[name]
277
+
278
+ # Try different parsing methods
279
+ if server_name_or_config.strip().startswith("--"):
280
+ # Command-line style arguments
281
+ name, config = self._parse_command_line_args(server_name_or_config)
282
+ else:
283
+ try:
284
+ # Try parsing as JSON
285
+ name, config = self._parse_json_config(server_name_or_config)
286
+ except json.JSONDecodeError:
287
+ logger.error(f"Failed to parse JSON config: {server_name_or_config}")
288
+ pass
289
+
209
290
  if not name:
210
291
  raise ValueError(
211
292
  "MCP server name is not available in MCP_BUILD_IN_SERVERS or external servers")
@@ -214,10 +295,13 @@ class McpServer:
214
295
  if not config:
215
296
  raise ValueError(f"MCP server {name} config is not available")
216
297
 
217
- await hub.add_server_config(name, config)
218
- return McpResponse(result=f"Successfully installed MCP server: {request.server_name_or_config}")
298
+ is_success = await hub.add_server_config(name, config)
299
+ if is_success:
300
+ return McpResponse(result=get_message_with_format("mcp_install_success", result=request.server_name_or_config))
301
+ else:
302
+ return McpResponse(result="", error=get_message_with_format("mcp_install_error", error="Failed to establish connection"))
219
303
  except Exception as e:
220
- return McpResponse(result="", error=f"Failed to install MCP server: {str(e)}")
304
+ return McpResponse(result="", error=get_message_with_format("mcp_install_error", error=str(e)))
221
305
 
222
306
  async def _process_request(self):
223
307
  hub = McpHub()
@@ -236,9 +320,11 @@ class McpServer:
236
320
  elif isinstance(request, McpRemoveRequest):
237
321
  try:
238
322
  await hub.remove_server_config(request.server_name)
239
- await self._response_queue.put(McpResponse(result=f"Successfully removed MCP server: {request.server_name}"))
323
+ await self._response_queue.put(McpResponse(
324
+ result=get_message_with_format("mcp_remove_success", result=request.server_name)))
240
325
  except Exception as e:
241
- await self._response_queue.put(McpResponse(result="", error=f"Failed to remove MCP server: {str(e)}"))
326
+ await self._response_queue.put(McpResponse(
327
+ result="", error=get_message_with_format("mcp_remove_error", error=str(e))))
242
328
 
243
329
  elif isinstance(request, McpListRequest):
244
330
  try:
@@ -253,20 +339,22 @@ class McpServer:
253
339
 
254
340
  # Combine results
255
341
  all_servers = builtin_servers + external_list
256
- result = "Available MCP servers:\n" + \
257
- "\n".join(all_servers)
342
+ result = "\n".join(all_servers)
258
343
 
259
344
  await self._response_queue.put(McpResponse(result=result))
260
345
  except Exception as e:
261
- await self._response_queue.put(McpResponse(result="", error=f"Failed to list servers: {str(e)}"))
346
+ await self._response_queue.put(McpResponse(
347
+ result="", error=get_message_with_format("mcp_list_builtin_error", error=str(e))))
262
348
 
263
349
  elif isinstance(request, McpListRunningRequest):
264
350
  try:
265
351
  running_servers = "\n".join(
266
352
  [f"- {server.name}" for server in hub.get_servers()])
267
- await self._response_queue.put(McpResponse(result=running_servers))
353
+ result = running_servers if running_servers else ""
354
+ await self._response_queue.put(McpResponse(result=result))
268
355
  except Exception as e:
269
- await self._response_queue.put(McpResponse(result="", error=f"Failed to list running servers: {str(e)}"))
356
+ await self._response_queue.put(McpResponse(
357
+ result="", error=get_message_with_format("mcp_list_running_error", error=str(e))))
270
358
 
271
359
  elif isinstance(request, McpRefreshRequest):
272
360
  try:
@@ -274,23 +362,36 @@ class McpServer:
274
362
  await hub.refresh_server_connection(request.name)
275
363
  else:
276
364
  await hub.initialize()
277
- await self._response_queue.put(McpResponse(result="Successfully refreshed MCP server connections"))
365
+ await self._response_queue.put(McpResponse(
366
+ result=get_message_with_format("mcp_refresh_success")))
278
367
  except Exception as e:
279
- await self._response_queue.put(McpResponse(result="", error=f"Failed to refresh MCP servers: {str(e)}"))
368
+ await self._response_queue.put(McpResponse(
369
+ result="", error=get_message_with_format("mcp_refresh_error", error=str(e))))
280
370
 
281
371
  else:
282
- llm = get_single_llm(request.model,product_mode=request.product_mode)
372
+ if not request.query.strip():
373
+ await self._response_queue.put(McpResponse(
374
+ result="", error=get_message_with_format("mcp_query_empty")))
375
+ continue
376
+
377
+ llm = get_single_llm(request.model, product_mode=request.product_mode)
283
378
  mcp_executor = McpExecutor(hub, llm)
284
379
  conversations = [
285
380
  {"role": "user", "content": request.query}]
286
381
  _, results = await mcp_executor.run(conversations)
382
+
287
383
  if not results:
288
- await self._response_queue.put(McpResponse(result="[No Result]", error="No results"))
289
- results_str = "\n\n".join(
290
- mcp_executor.format_mcp_result(result) for result in results)
291
- await self._response_queue.put(McpResponse(result=results_str))
384
+ await self._response_queue.put(McpResponse(
385
+ result=get_message_with_format("mcp_error_title"),
386
+ error="No results"))
387
+ else:
388
+ results_str = "\n\n".join(
389
+ mcp_executor.format_mcp_result(result) for result in results)
390
+ await self._response_queue.put(McpResponse(
391
+ result=get_message_with_format("mcp_response_title") + "\n" + results_str))
292
392
  except Exception as e:
293
- await self._response_queue.put(McpResponse(result="", error=str(e)))
393
+ await self._response_queue.put(McpResponse(
394
+ result="", error=get_message_with_format("mcp_error_title") + ": " + str(e)))
294
395
 
295
396
  def send_request(self, request: McpRequest) -> McpResponse:
296
397
  async def _send():