auto-coder 0.1.334__py3-none-any.whl → 0.1.335__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.

Files changed (43) hide show
  1. {auto_coder-0.1.334.dist-info → auto_coder-0.1.335.dist-info}/METADATA +1 -1
  2. {auto_coder-0.1.334.dist-info → auto_coder-0.1.335.dist-info}/RECORD +43 -26
  3. autocoder/agent/agentic_edit.py +833 -0
  4. autocoder/agent/agentic_edit_tools/__init__.py +28 -0
  5. autocoder/agent/agentic_edit_tools/ask_followup_question_tool_resolver.py +32 -0
  6. autocoder/agent/agentic_edit_tools/attempt_completion_tool_resolver.py +29 -0
  7. autocoder/agent/agentic_edit_tools/base_tool_resolver.py +29 -0
  8. autocoder/agent/agentic_edit_tools/execute_command_tool_resolver.py +84 -0
  9. autocoder/agent/agentic_edit_tools/list_code_definition_names_tool_resolver.py +75 -0
  10. autocoder/agent/agentic_edit_tools/list_files_tool_resolver.py +62 -0
  11. autocoder/agent/agentic_edit_tools/plan_mode_respond_tool_resolver.py +30 -0
  12. autocoder/agent/agentic_edit_tools/read_file_tool_resolver.py +36 -0
  13. autocoder/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +95 -0
  14. autocoder/agent/agentic_edit_tools/search_files_tool_resolver.py +70 -0
  15. autocoder/agent/agentic_edit_tools/use_mcp_tool_resolver.py +55 -0
  16. autocoder/agent/agentic_edit_tools/write_to_file_tool_resolver.py +98 -0
  17. autocoder/agent/agentic_edit_types.py +124 -0
  18. autocoder/auto_coder.py +5 -1
  19. autocoder/auto_coder_runner.py +5 -1
  20. autocoder/chat_auto_coder_lang.py +18 -2
  21. autocoder/commands/tools.py +5 -1
  22. autocoder/common/__init__.py +1 -0
  23. autocoder/common/auto_coder_lang.py +40 -8
  24. autocoder/common/code_auto_generate_diff.py +1 -1
  25. autocoder/common/code_auto_generate_editblock.py +1 -1
  26. autocoder/common/code_auto_generate_strict_diff.py +1 -1
  27. autocoder/common/mcp_hub.py +185 -2
  28. autocoder/common/mcp_server.py +243 -306
  29. autocoder/common/mcp_server_install.py +269 -0
  30. autocoder/common/mcp_server_types.py +169 -0
  31. autocoder/common/stream_out_type.py +3 -0
  32. autocoder/common/v2/code_auto_generate.py +1 -1
  33. autocoder/common/v2/code_auto_generate_diff.py +1 -1
  34. autocoder/common/v2/code_auto_generate_editblock.py +1 -1
  35. autocoder/common/v2/code_auto_generate_strict_diff.py +1 -1
  36. autocoder/common/v2/code_editblock_manager.py +151 -178
  37. autocoder/compilers/provided_compiler.py +3 -2
  38. autocoder/shadows/shadow_manager.py +1 -1
  39. autocoder/version.py +1 -1
  40. {auto_coder-0.1.334.dist-info → auto_coder-0.1.335.dist-info}/LICENSE +0 -0
  41. {auto_coder-0.1.334.dist-info → auto_coder-0.1.335.dist-info}/WHEEL +0 -0
  42. {auto_coder-0.1.334.dist-info → auto_coder-0.1.335.dist-info}/entry_points.txt +0 -0
  43. {auto_coder-0.1.334.dist-info → auto_coder-0.1.335.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,98 @@
1
+ import os
2
+ import re
3
+ from typing import Dict, Any, Optional, List, Tuple
4
+ from .base_tool_resolver import BaseToolResolver
5
+ from autocoder.agent.agentic_edit_types import WriteToFileTool, ToolResult # Import ToolResult from types
6
+ from loguru import logger
7
+
8
+
9
+ class WriteToFileToolResolver(BaseToolResolver):
10
+ def __init__(self, agent: Optional[Any], tool: WriteToFileTool, args: Dict[str, Any]):
11
+ super().__init__(agent, tool, args)
12
+ self.tool: WriteToFileTool = tool # For type hinting
13
+
14
+ def parse_diff(self, diff_content: str) -> List[Tuple[str, str]]:
15
+ """
16
+ Parses the diff content into a list of (search_block, replace_block) tuples.
17
+ """
18
+ blocks = []
19
+ # Regex to find SEARCH/REPLACE blocks, handling potential variations in line endings
20
+ pattern = re.compile(r"<<<<<<< SEARCH\r?\n(.*?)\r?\n=======\r?\n(.*?)\r?\n>>>>>>> REPLACE", re.DOTALL)
21
+ matches = pattern.findall(diff_content)
22
+ for search_block, replace_block in matches:
23
+ blocks.append((search_block, replace_block))
24
+ if not matches and diff_content.strip():
25
+ logger.warning(f"Could not parse any SEARCH/REPLACE blocks from diff: {diff_content}")
26
+ return blocks
27
+
28
+ def resolve(self) -> ToolResult:
29
+ file_path = self.tool.path
30
+ content = self.tool.content
31
+ source_dir = self.args.source_dir or "."
32
+ absolute_path = os.path.abspath(os.path.join(source_dir, file_path))
33
+
34
+ # Security check: ensure the path is within the source directory
35
+ if not absolute_path.startswith(os.path.abspath(source_dir)):
36
+ return ToolResult(success=False, message=f"Error: Access denied. Attempted to write file outside the project directory: {file_path}")
37
+
38
+ try:
39
+ # Create directories if they don't exist
40
+ os.makedirs(os.path.dirname(absolute_path), exist_ok=True)
41
+
42
+ # Check if the content contains SEARCH/REPLACE blocks
43
+ parsed_blocks = self.parse_diff(content)
44
+ if parsed_blocks:
45
+ # If file exists, read its current content
46
+ if os.path.exists(absolute_path):
47
+ try:
48
+ with open(absolute_path, 'r', encoding='utf-8', errors='replace') as f:
49
+ original_content = f.read()
50
+ except Exception as e:
51
+ logger.error(f"Error reading existing file '{file_path}' for diff apply: {str(e)}")
52
+ return ToolResult(success=False, message=f"An error occurred while reading the existing file: {str(e)}")
53
+ else:
54
+ # If file does not exist, start with empty content
55
+ original_content = ""
56
+
57
+ current_content = original_content
58
+ applied_count = 0
59
+ errors = []
60
+
61
+ for i, (search_block, replace_block) in enumerate(parsed_blocks):
62
+ start_index = current_content.find(search_block)
63
+ if start_index != -1:
64
+ current_content = (
65
+ current_content[:start_index]
66
+ + replace_block
67
+ + current_content[start_index + len(search_block):]
68
+ )
69
+ applied_count += 1
70
+ logger.info(f"Applied SEARCH/REPLACE block {i+1} in file {file_path}")
71
+ else:
72
+ error_message = f"SEARCH block {i+1} not found in current content. Search block:\n---\n{search_block}\n---"
73
+ logger.warning(error_message)
74
+ errors.append(error_message)
75
+ # Continue with next block
76
+
77
+ try:
78
+ with open(absolute_path, 'w', encoding='utf-8') as f:
79
+ f.write(current_content)
80
+ message = f"Successfully applied {applied_count}/{len(parsed_blocks)} changes to file: {file_path}."
81
+ if errors:
82
+ message += "\nWarnings:\n" + "\n".join(errors)
83
+ logger.info(message)
84
+ return ToolResult(success=True, message=message, content=current_content)
85
+ except Exception as e:
86
+ logger.error(f"Error writing replaced content to file '{file_path}': {str(e)}")
87
+ return ToolResult(success=False, message=f"An error occurred while writing the modified file: {str(e)}")
88
+ else:
89
+ # No diff blocks detected, treat as full content overwrite
90
+ with open(absolute_path, 'w', encoding='utf-8') as f:
91
+ f.write(content)
92
+
93
+ logger.info(f"Successfully wrote to file: {file_path}")
94
+ return ToolResult(success=True, message=f"Successfully wrote to file: {file_path}", content=content)
95
+
96
+ except Exception as e:
97
+ logger.error(f"Error writing to file '{file_path}': {str(e)}")
98
+ return ToolResult(success=False, message=f"An error occurred while writing to the file: {str(e)}")
@@ -0,0 +1,124 @@
1
+ from pydantic import BaseModel
2
+ from typing import List, Dict, Any, Callable, Optional, Type
3
+ from pydantic import SkipValidation
4
+
5
+
6
+ # Result class used by Tool Resolvers
7
+ class ToolResult(BaseModel):
8
+ success: bool
9
+ message: str
10
+ content: Any = None # Can store file content, command output, etc.
11
+
12
+ # Pydantic Models for Tools
13
+ class BaseTool(BaseModel):
14
+ pass
15
+
16
+ class ExecuteCommandTool(BaseTool):
17
+ command: str
18
+ requires_approval: bool
19
+
20
+ class ReadFileTool(BaseTool):
21
+ path: str
22
+
23
+ class WriteToFileTool(BaseTool):
24
+ path: str
25
+ content: str
26
+
27
+ class ReplaceInFileTool(BaseTool):
28
+ path: str
29
+ diff: str
30
+
31
+ class SearchFilesTool(BaseTool):
32
+ path: str
33
+ regex: str
34
+ file_pattern: Optional[str] = None
35
+
36
+ class ListFilesTool(BaseTool):
37
+ path: str
38
+ recursive: Optional[bool] = False
39
+
40
+ class ListCodeDefinitionNamesTool(BaseTool):
41
+ path: str
42
+
43
+ class AskFollowupQuestionTool(BaseTool):
44
+ question: str
45
+ options: Optional[List[str]] = None
46
+
47
+ class AttemptCompletionTool(BaseTool):
48
+ result: str
49
+ command: Optional[str] = None
50
+
51
+ class PlanModeRespondTool(BaseTool):
52
+ response: str
53
+ options: Optional[List[str]] = None
54
+
55
+ class UseMcpTool(BaseTool):
56
+ server_name: str
57
+ tool_name: str
58
+ arguments: Dict[str, Any]
59
+
60
+ class PlainTextOutput(BaseModel):
61
+ text: str
62
+
63
+
64
+ # Mapping from tool tag names to Pydantic models
65
+ TOOL_MODEL_MAP: Dict[str, Type[BaseTool]] = {
66
+ "execute_command": ExecuteCommandTool,
67
+ "read_file": ReadFileTool,
68
+ "write_to_file": WriteToFileTool,
69
+ "replace_in_file": ReplaceInFileTool,
70
+ "search_files": SearchFilesTool,
71
+ "list_files": ListFilesTool,
72
+ "list_code_definition_names": ListCodeDefinitionNamesTool,
73
+ "ask_followup_question": AskFollowupQuestionTool,
74
+ "attempt_completion": AttemptCompletionTool,
75
+ "plan_mode_respond": PlanModeRespondTool,
76
+ "use_mcp_tool": UseMcpTool,
77
+ }
78
+
79
+
80
+ class AgenticEditRequest(BaseModel):
81
+ user_input: str
82
+
83
+
84
+ class FileOperation(BaseModel):
85
+ path: str
86
+ operation: str # e.g., "MODIFY", "REFERENCE", "ADD", "REMOVE"
87
+ class MemoryConfig(BaseModel):
88
+ """
89
+ A model to encapsulate memory configuration and operations.
90
+ """
91
+
92
+ memory: Dict[str, Any]
93
+ save_memory_func: SkipValidation[Callable]
94
+
95
+ class Config:
96
+ arbitrary_types_allowed = True
97
+
98
+
99
+ class CommandConfig(BaseModel):
100
+ coding: SkipValidation[Callable]
101
+ chat: SkipValidation[Callable]
102
+ add_files: SkipValidation[Callable]
103
+ remove_files: SkipValidation[Callable]
104
+ index_build: SkipValidation[Callable]
105
+ index_query: SkipValidation[Callable]
106
+ list_files: SkipValidation[Callable]
107
+ ask: SkipValidation[Callable]
108
+ revert: SkipValidation[Callable]
109
+ commit: SkipValidation[Callable]
110
+ help: SkipValidation[Callable]
111
+ exclude_dirs: SkipValidation[Callable]
112
+ summon: SkipValidation[Callable]
113
+ design: SkipValidation[Callable]
114
+ mcp: SkipValidation[Callable]
115
+ models: SkipValidation[Callable]
116
+ lib: SkipValidation[Callable]
117
+ execute_shell_command: SkipValidation[Callable]
118
+ generate_shell_command: SkipValidation[Callable]
119
+ conf_export: SkipValidation[Callable]
120
+ conf_import: SkipValidation[Callable]
121
+ index_export: SkipValidation[Callable]
122
+ index_import: SkipValidation[Callable]
123
+ exclude_files: SkipValidation[Callable]
124
+
autocoder/auto_coder.py CHANGED
@@ -52,6 +52,11 @@ from autocoder.privacy.model_filter import ModelPathFilter
52
52
  from autocoder.common.result_manager import ResultManager
53
53
  from autocoder.events.event_manager_singleton import get_event_manager
54
54
  from autocoder.events import event_content as EventContentCreator
55
+ from autocoder.common.mcp_server import get_mcp_server
56
+ from autocoder.common.mcp_server_types import (
57
+ McpRequest, McpInstallRequest, McpRemoveRequest, McpListRequest,
58
+ McpListRunningRequest, McpRefreshRequest
59
+ )
55
60
 
56
61
  console = Console()
57
62
 
@@ -1254,7 +1259,6 @@ def main(input_args: Optional[List[str]] = None):
1254
1259
  v = (item for item in response)
1255
1260
 
1256
1261
  elif "mcp" in commands_info:
1257
- from autocoder.common.mcp_server import get_mcp_server, McpRequest, McpInstallRequest, McpRemoveRequest, McpListRequest, McpListRunningRequest, McpRefreshRequest
1258
1262
  mcp_server = get_mcp_server()
1259
1263
 
1260
1264
  pos_args = commands_info["mcp"].get("args", [])
@@ -40,7 +40,11 @@ import git
40
40
  from autocoder.common import git_utils
41
41
  from autocoder.chat_auto_coder_lang import get_message
42
42
  from autocoder.agent.auto_guess_query import AutoGuessQuery
43
- from autocoder.common.mcp_server import get_mcp_server, McpRequest, McpInstallRequest, McpRemoveRequest, McpListRequest, McpListRunningRequest, McpRefreshRequest,McpServerInfoRequest
43
+ from autocoder.common.mcp_server import get_mcp_server
44
+ from autocoder.common.mcp_server_types import (
45
+ McpRequest, McpInstallRequest, McpRemoveRequest, McpListRequest,
46
+ McpListRunningRequest, McpRefreshRequest, McpServerInfoRequest
47
+ )
44
48
  import byzerllm
45
49
  from byzerllm.utils import format_str_jinja2
46
50
  from autocoder.common.memory_manager import get_global_memory_file_paths
@@ -30,6 +30,14 @@ MESSAGES = {
30
30
  "en": "Available builtin MCP servers:",
31
31
  "zh": "可用的内置 MCP 服务器:"
32
32
  },
33
+ "mcp_list_external_title": {
34
+ "en": "Available external MCP servers:",
35
+ "zh": "可用的外部 MCP 服务器:"
36
+ },
37
+ "mcp_list_marketplace_title": {
38
+ "en": "Available marketplace MCP servers:",
39
+ "zh": "可用的市场 MCP 服务器:"
40
+ },
33
41
  "mcp_refresh_error": {
34
42
  "en": "Error refreshing MCP servers: {{error}}",
35
43
  "zh": "刷新 MCP 服务器时出错:{{error}}"
@@ -543,8 +551,8 @@ MESSAGES = {
543
551
  "zh": "用法: /plugins <命令>\n可用的子命令:\n /plugins /list - 列出所有可用插件\n /plugins /load <名称> - 加载一个插件\n /plugins /unload <名称> - 卸载一个插件\n /plugins/dirs - 列出插件目录\n /plugins/dirs /add <路径> - 添加一个插件目录\n /plugins/dirs /remove <路径> - 移除一个插件目录\n /plugins/dirs /clear - 清除所有插件目录"
544
552
  },
545
553
  "mcp_server_info_error": {
546
- "en": "Error getting MCP server info: {{ error }}",
547
- "zh": "获取MCP服务器信息时出错: {{ error }}"
554
+ "en": "Error getting MCP server info: {{error}}",
555
+ "zh": "获取 MCP 服务器信息时出错:{{error}}"
548
556
  },
549
557
  "mcp_server_info_title": {
550
558
  "en": "Connected MCP Server Info",
@@ -553,6 +561,14 @@ MESSAGES = {
553
561
  "active_context_desc": {
554
562
  "en": "Manage active context tasks, list all tasks and their status",
555
563
  "zh": "管理活动上下文任务,列出所有任务及其状态"
564
+ },
565
+ "marketplace_add_success": {
566
+ "en": "Successfully added marketplace item: {{name}}",
567
+ "zh": "成功添加市场项目:{{name}}"
568
+ },
569
+ "marketplace_add_error": {
570
+ "en": "Error adding marketplace item: {{name}} - {{error}}",
571
+ "zh": "添加市场项目时出错:{{name}} - {{error}}"
556
572
  }
557
573
  }
558
574
 
@@ -42,6 +42,11 @@ from autocoder.events.event_manager_singleton import get_event_manager
42
42
  from autocoder.events import event_content as EventContentCreator
43
43
  from autocoder.linters.linter_factory import LinterFactory, lint_file, lint_project, format_lint_result
44
44
  import traceback
45
+ from autocoder.common.mcp_server import get_mcp_server
46
+ from autocoder.common.mcp_server_types import (
47
+ McpRequest, McpInstallRequest, McpRemoveRequest, McpListRequest,
48
+ McpListRunningRequest, McpRefreshRequest
49
+ )
45
50
 
46
51
 
47
52
  @byzerllm.prompt()
@@ -76,7 +81,6 @@ class AutoCommandTools:
76
81
  self.printer = Printer()
77
82
 
78
83
  def execute_mcp_server(self, query: str) -> str:
79
- from autocoder.common.mcp_server import get_mcp_server, McpRequest, McpInstallRequest, McpRemoveRequest, McpListRequest, McpListRunningRequest, McpRefreshRequest
80
84
  mcp_server = get_mcp_server()
81
85
  response = mcp_server.send_request(
82
86
  McpRequest(
@@ -416,6 +416,7 @@ class AutoCoderArgs(pydantic.BaseModel):
416
416
  event_file: Optional[str] = None
417
417
 
418
418
  enable_active_context: Optional[bool] = False
419
+ enable_active_context_in_generate: Optional[bool] = False
419
420
 
420
421
  generate_max_rounds: Optional[int] = 5
421
422
 
@@ -453,7 +453,7 @@ MESSAGES = {
453
453
  "quick_filter_too_long": {
454
454
  "en": "⚠️ index file is too large ({{ tokens_len }}/{{ max_tokens }}). The query will be split into {{ split_size }} chunks.",
455
455
  "zh": "⚠️ 索引文件过大 ({{ tokens_len }}/{{ max_tokens }})。查询将被分成 {{ split_size }} 个部分执行。"
456
- },
456
+ },
457
457
  "quick_filter_tokens_len": {
458
458
  "en": "📊 Current index size: {{ tokens_len }} tokens",
459
459
  "zh": "📊 当前索引大小: {{ tokens_len }} tokens"
@@ -787,13 +787,45 @@ MESSAGES = {
787
787
  "zh": "已达到最大未合并代码块修复尝试次数"
788
788
  },
789
789
  "agenticFilterContext": {
790
- "en": "Start to find context...",
791
- "zh": "开始智能查找上下文...."
792
- },
793
- "agenticFilterContextFinished": {
794
- "en": "End to find context...",
795
- "zh": "结束智能查找上下文...."
796
- }
790
+ "en": "Start to find context...",
791
+ "zh": "开始智能查找上下文...."
792
+ },
793
+ "agenticFilterContextFinished": {
794
+ "en": "End to find context...",
795
+ "zh": "结束智能查找上下文...."
796
+ },
797
+ "/context/check/start":{
798
+ "en": "Starting missing context checking process.",
799
+ "zh": "开始缺失上下文检查过程."
800
+ },
801
+ "/context/check/end": {
802
+ "en": "Finished missing context checking process.",
803
+ "zh": "结束缺失上下文检查过程."
804
+ },
805
+ "/unmerged_blocks/check/start": {
806
+ "en": "Starting unmerged blocks checking process.",
807
+ "zh": "开始未合并代码检查过程."
808
+ },
809
+ "/unmerged_blocks/check/end": {
810
+ "en": "Finished unmerged blocks checking process.",
811
+ "zh": "结束未合并代码检查过程."
812
+ },
813
+ "/lint/check/start": {
814
+ "en": "Starting lint error checking process.",
815
+ "zh": "开始代码质量检查过程."
816
+ },
817
+ "/lint/check/end": {
818
+ "en": "Finished lint error checking process.",
819
+ "zh": "结束代码质量检查过程."
820
+ },
821
+ "/compile/check/start": {
822
+ "en": "Starting compile error checking process.",
823
+ "zh": "开始编译错误检查过程."
824
+ },
825
+ "/compile/check/end": {
826
+ "en": "Finished compile error checking process.",
827
+ "zh": "结束编译错误检查过程."
828
+ }
797
829
  }
798
830
 
799
831
 
@@ -329,7 +329,7 @@ class CodeAutoGenerateDiff:
329
329
  # 获取包上下文信息
330
330
  package_context = ""
331
331
 
332
- if self.args.enable_active_context:
332
+ if self.args.enable_active_context and self.args.enable_active_context_in_generate:
333
333
  # 初始化活动上下文管理器
334
334
  active_context_manager = ActiveContextManager(self.llm, self.args.source_dir)
335
335
  # 获取活动上下文信息
@@ -438,7 +438,7 @@ class CodeAutoGenerateEditBlock:
438
438
  # 获取包上下文信息
439
439
  package_context = ""
440
440
 
441
- if self.args.enable_active_context:
441
+ if self.args.enable_active_context and self.args.enable_active_context_in_generate:
442
442
  # 获取活动上下文信息
443
443
  result = active_context_manager.load_active_contexts_for_files(
444
444
  [source.module_name for source in source_code_list.sources]
@@ -299,7 +299,7 @@ class CodeAutoGenerateStrictDiff:
299
299
  # 获取包上下文信息
300
300
  package_context = ""
301
301
 
302
- if self.args.enable_active_context:
302
+ if self.args.enable_active_context and self.args.enable_active_context_in_generate:
303
303
  # 初始化活动上下文管理器
304
304
  active_context_manager = ActiveContextManager(self.llm, self.args.source_dir)
305
305
  # 获取活动上下文信息
@@ -13,6 +13,7 @@ from pydantic import BaseModel, Field
13
13
  from loguru import logger
14
14
  from contextlib import AsyncExitStack
15
15
  from datetime import timedelta
16
+ from autocoder.common.mcp_server_types import MarketplaceMCPServerItem
16
17
 
17
18
  try:
18
19
  from mcp import ClientSession
@@ -42,7 +43,6 @@ class McpResource(BaseModel):
42
43
  description: Optional[str] = None
43
44
  mime_type: Optional[str] = None
44
45
 
45
-
46
46
  class McpResourceTemplate(BaseModel):
47
47
  """Represents an MCP resource template"""
48
48
 
@@ -133,7 +133,7 @@ class McpHub:
133
133
  cls._instance._initialized = False
134
134
  return cls._instance
135
135
 
136
- def __init__(self, settings_path: Optional[str] = None):
136
+ def __init__(self, settings_path: Optional[str] = None, marketplace_path: Optional[str] = None):
137
137
  if self._initialized:
138
138
  return
139
139
  """Initialize the MCP Hub with a path to settings file"""
@@ -141,6 +141,12 @@ class McpHub:
141
141
  self.settings_path = Path.home() / ".auto-coder" / "mcp" / "settings.json"
142
142
  else:
143
143
  self.settings_path = Path(settings_path)
144
+
145
+ if marketplace_path is None:
146
+ self.marketplace_path = Path.home() / ".auto-coder" / "mcp" / "marketplace.json"
147
+ else:
148
+ self.marketplace_path = Path(marketplace_path)
149
+
144
150
  self.connections: Dict[str, McpConnection] = {}
145
151
  self.is_connecting = False
146
152
  self.exit_stacks: Dict[str, AsyncExitStack] = {}
@@ -149,6 +155,11 @@ class McpHub:
149
155
  self.settings_path.parent.mkdir(parents=True, exist_ok=True)
150
156
  if not self.settings_path.exists():
151
157
  self._write_default_settings()
158
+
159
+ # Ensure marketplace file exists
160
+ self.marketplace_path.parent.mkdir(parents=True, exist_ok=True)
161
+ if not self.marketplace_path.exists():
162
+ self._write_default_marketplace()
152
163
 
153
164
  self._initialized = True
154
165
 
@@ -157,6 +168,178 @@ class McpHub:
157
168
  default_settings = {"mcpServers": {}}
158
169
  with open(self.settings_path, "w", encoding="utf-8") as f:
159
170
  json.dump(default_settings, f, indent=2)
171
+
172
+ def _write_default_marketplace(self):
173
+ """Write default marketplace file"""
174
+ default_marketplace = {"mcpServers": []}
175
+ with open(self.marketplace_path, "w", encoding="utf-8") as f:
176
+ json.dump(default_marketplace, f, indent=2)
177
+
178
+ def _read_marketplace(self) -> Dict[str, List[Dict[str, Any]]]:
179
+ """Read marketplace file"""
180
+ try:
181
+ with open(self.marketplace_path,"r", encoding="utf-8") as f:
182
+ return json.load(f)
183
+ except Exception as e:
184
+ logger.error(f"Failed to read marketplace: {e}")
185
+ return {"mcpServers": []}
186
+
187
+ def get_marketplace_items(self) -> List[MarketplaceMCPServerItem]:
188
+ """Get all marketplace items"""
189
+ data = self._read_marketplace()
190
+ return [MarketplaceMCPServerItem(**item) for item in data.get("mcpServers", [])]
191
+
192
+ def get_marketplace_item(self, name: str) -> Optional[MarketplaceMCPServerItem]:
193
+ """Get a marketplace item by name"""
194
+ items = self.get_marketplace_items()
195
+ for item in items:
196
+ if item.name == name:
197
+ return item
198
+ return None
199
+
200
+ async def add_marketplace_item(self, item: MarketplaceMCPServerItem) -> bool:
201
+ """
202
+ Add a new marketplace item
203
+
204
+ Args:
205
+ item: MarketplaceMCPServerItem to add
206
+
207
+ Returns:
208
+ bool: True if successful, False otherwise
209
+ """
210
+ try:
211
+ # Check if item with this name already exists
212
+ existing = self.get_marketplace_item(item.name)
213
+ if existing:
214
+ logger.warning(f"Marketplace item with name {item.name} already exists")
215
+ return False
216
+
217
+ # Add the new item
218
+ data = self._read_marketplace()
219
+ data["mcpServers"].append(item.dict())
220
+
221
+ # Write back to file
222
+ with open(self.marketplace_path, "w", encoding="utf-8") as f:
223
+ json.dump(data, f, indent=2, ensure_ascii=False)
224
+
225
+ logger.info(f"Added marketplace item: {item.name}")
226
+ return True
227
+ except Exception as e:
228
+ logger.error(f"Failed to add marketplace item: {e}")
229
+ return False
230
+
231
+ async def update_marketplace_item(self, name: str, updated_item: MarketplaceMCPServerItem) -> bool:
232
+ """
233
+ Update an existing marketplace item
234
+
235
+ Args:
236
+ name: Name of the item to update
237
+ updated_item: Updated MarketplaceMCPServerItem
238
+
239
+ Returns:
240
+ bool: True if successful, False otherwise
241
+ """
242
+ try:
243
+ data = self._read_marketplace()
244
+ items = data.get("mcpServers", [])
245
+
246
+ # Find the item to update
247
+ for i, item in enumerate(items):
248
+ if item.get("name") == name:
249
+ # Update the item
250
+ items[i] = updated_item.model_dump()
251
+
252
+ # Write back to file
253
+ with open(self.marketplace_path, "w", encoding="utf-8") as f:
254
+ json.dump(data, f, indent=2, ensure_ascii=False)
255
+
256
+ logger.info(f"Updated marketplace item: {name}")
257
+ return True
258
+
259
+ logger.warning(f"Marketplace item with name {name} not found")
260
+ return False
261
+ except Exception as e:
262
+ logger.error(f"Failed to update marketplace item: {e}")
263
+ return False
264
+
265
+ async def remove_marketplace_item(self, name: str) -> bool:
266
+ """
267
+ Remove a marketplace item
268
+
269
+ Args:
270
+ name: Name of the item to remove
271
+
272
+ Returns:
273
+ bool: True if successful, False otherwise
274
+ """
275
+ try:
276
+ data = self._read_marketplace()
277
+ items = data.get("mcpServers", [])
278
+
279
+ # Find and remove the item
280
+ for i, item in enumerate(items):
281
+ if item.get("name") == name:
282
+ del items[i]
283
+
284
+ # Write back to file
285
+ with open(self.marketplace_path, "w", encoding="utf-8") as f:
286
+ json.dump(data, f, indent=2, ensure_ascii=False)
287
+
288
+ logger.info(f"Removed marketplace item: {name}")
289
+ return True
290
+
291
+ logger.warning(f"Marketplace item with name {name} not found")
292
+ return False
293
+ except Exception as e:
294
+ logger.error(f"Failed to remove marketplace item: {e}")
295
+ return False
296
+
297
+ async def apply_marketplace_item(self, name: str) -> bool:
298
+ """
299
+ Apply a marketplace item to server config
300
+
301
+ Args:
302
+ name: Name of the item to apply
303
+
304
+ Returns:
305
+ bool: True if successful, False otherwise
306
+ """
307
+ try:
308
+ item = self.get_marketplace_item(name)
309
+ if not item:
310
+ logger.warning(f"Marketplace item with name {name} not found")
311
+ return False
312
+
313
+ # Convert marketplace item to server config
314
+ config = {}
315
+
316
+ if item.mcp_type == "command":
317
+ config = {
318
+ "command": item.command,
319
+ "args": item.args,
320
+ "env": item.env,
321
+ "transport": {
322
+ "type": "stdio",
323
+ "endpoint": ""
324
+ }
325
+ }
326
+ elif item.mcp_type == "sse":
327
+ config = {
328
+ "transport": {
329
+ "type": "sse",
330
+ "endpoint": item.url
331
+ }
332
+ }
333
+ else:
334
+ logger.error(f"Unknown MCP type: {item.mcp_type}")
335
+ return False
336
+
337
+ # Add server config
338
+ result = await self.add_server_config(name, config)
339
+ return result
340
+ except Exception as e:
341
+ logger.error(f"Failed to apply marketplace item: {e}")
342
+ return False
160
343
 
161
344
  async def add_server_config(self, name: str, config: Dict[str, Any]) -> None:
162
345
  """