deepcode-hku 1.0.1__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.
- cli/__init__.py +18 -0
- cli/cli_app.py +296 -0
- cli/cli_interface.py +744 -0
- cli/cli_launcher.py +155 -0
- cli/main_cli.py +243 -0
- cli/workflows/__init__.py +11 -0
- cli/workflows/cli_workflow_adapter.py +336 -0
- deepcode.py +219 -0
- deepcode_hku-1.0.1.dist-info/METADATA +695 -0
- deepcode_hku-1.0.1.dist-info/RECORD +44 -0
- deepcode_hku-1.0.1.dist-info/WHEEL +5 -0
- deepcode_hku-1.0.1.dist-info/entry_points.txt +2 -0
- deepcode_hku-1.0.1.dist-info/licenses/LICENSE +21 -0
- deepcode_hku-1.0.1.dist-info/top_level.txt +6 -0
- tools/__init__.py +0 -0
- tools/code_implementation_server.py +1045 -0
- tools/code_indexer.py +1657 -0
- tools/code_reference_indexer.py +486 -0
- tools/command_executor.py +324 -0
- tools/git_command.py +356 -0
- tools/pdf_converter.py +640 -0
- tools/pdf_downloader.py +1370 -0
- tools/pdf_utils.py +52 -0
- ui/__init__.py +43 -0
- ui/app.py +13 -0
- ui/components.py +1450 -0
- ui/handlers.py +773 -0
- ui/layout.py +106 -0
- ui/streamlit_app.py +38 -0
- ui/styles.py +2116 -0
- utils/__init__.py +17 -0
- utils/cli_interface.py +459 -0
- utils/dialogue_logger.py +671 -0
- utils/file_processor.py +426 -0
- utils/simple_llm_logger.py +198 -0
- workflows/__init__.py +31 -0
- workflows/agent_orchestration_engine.py +1371 -0
- workflows/agents/__init__.py +13 -0
- workflows/agents/code_implementation_agent.py +1093 -0
- workflows/agents/memory_agent_concise.py +923 -0
- workflows/agents/memory_agent_concise_index.py +935 -0
- workflows/code_implementation_workflow.py +924 -0
- workflows/code_implementation_workflow_index.py +931 -0
- workflows/codebase_index_workflow.py +726 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Command Executor MCP Tool / 命令执行器 MCP 工具
|
|
4
|
+
|
|
5
|
+
专门负责执行LLM生成的shell命令来创建文件树结构
|
|
6
|
+
Specialized in executing LLM-generated shell commands to create file tree structures
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Dict
|
|
12
|
+
from mcp.server.models import InitializationOptions
|
|
13
|
+
import mcp.types as types
|
|
14
|
+
from mcp.server import NotificationOptions, Server
|
|
15
|
+
import mcp.server.stdio
|
|
16
|
+
|
|
17
|
+
# 创建MCP服务器实例 / Create MCP server instance
|
|
18
|
+
app = Server("command-executor")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.list_tools()
|
|
22
|
+
async def handle_list_tools() -> list[types.Tool]:
|
|
23
|
+
"""
|
|
24
|
+
列出可用工具 / List available tools
|
|
25
|
+
"""
|
|
26
|
+
return [
|
|
27
|
+
types.Tool(
|
|
28
|
+
name="execute_commands",
|
|
29
|
+
description="""
|
|
30
|
+
执行shell命令列表来创建文件树结构
|
|
31
|
+
Execute shell command list to create file tree structure
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
commands: 要执行的shell命令列表(每行一个命令)
|
|
35
|
+
working_directory: 执行命令的工作目录
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
命令执行结果和详细报告
|
|
39
|
+
""",
|
|
40
|
+
inputSchema={
|
|
41
|
+
"type": "object",
|
|
42
|
+
"properties": {
|
|
43
|
+
"commands": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"title": "Commands",
|
|
46
|
+
"description": "要执行的shell命令列表,每行一个命令",
|
|
47
|
+
},
|
|
48
|
+
"working_directory": {
|
|
49
|
+
"type": "string",
|
|
50
|
+
"title": "Working Directory",
|
|
51
|
+
"description": "执行命令的工作目录",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
"required": ["commands", "working_directory"],
|
|
55
|
+
},
|
|
56
|
+
),
|
|
57
|
+
types.Tool(
|
|
58
|
+
name="execute_single_command",
|
|
59
|
+
description="""
|
|
60
|
+
执行单个shell命令
|
|
61
|
+
Execute single shell command
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
command: 要执行的单个命令
|
|
65
|
+
working_directory: 执行命令的工作目录
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
命令执行结果
|
|
69
|
+
""",
|
|
70
|
+
inputSchema={
|
|
71
|
+
"type": "object",
|
|
72
|
+
"properties": {
|
|
73
|
+
"command": {
|
|
74
|
+
"type": "string",
|
|
75
|
+
"title": "Command",
|
|
76
|
+
"description": "要执行的单个shell命令",
|
|
77
|
+
},
|
|
78
|
+
"working_directory": {
|
|
79
|
+
"type": "string",
|
|
80
|
+
"title": "Working Directory",
|
|
81
|
+
"description": "执行命令的工作目录",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
"required": ["command", "working_directory"],
|
|
85
|
+
},
|
|
86
|
+
),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@app.call_tool()
|
|
91
|
+
async def handle_call_tool(name: str, arguments: dict) -> list[types.TextContent]:
|
|
92
|
+
"""
|
|
93
|
+
处理工具调用 / Handle tool calls
|
|
94
|
+
"""
|
|
95
|
+
try:
|
|
96
|
+
if name == "execute_commands":
|
|
97
|
+
return await execute_command_batch(
|
|
98
|
+
arguments.get("commands", ""), arguments.get("working_directory", ".")
|
|
99
|
+
)
|
|
100
|
+
elif name == "execute_single_command":
|
|
101
|
+
return await execute_single_command(
|
|
102
|
+
arguments.get("command", ""), arguments.get("working_directory", ".")
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
raise ValueError(f"未知工具 / Unknown tool: {name}")
|
|
106
|
+
|
|
107
|
+
except Exception as e:
|
|
108
|
+
return [
|
|
109
|
+
types.TextContent(
|
|
110
|
+
type="text",
|
|
111
|
+
text=f"工具执行错误 / Error executing tool {name}: {str(e)}",
|
|
112
|
+
)
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def execute_command_batch(
|
|
117
|
+
commands: str, working_directory: str
|
|
118
|
+
) -> list[types.TextContent]:
|
|
119
|
+
"""
|
|
120
|
+
执行多个shell命令 / Execute multiple shell commands
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
commands: 命令列表,每行一个命令 / Command list, one command per line
|
|
124
|
+
working_directory: 工作目录 / Working directory
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
执行结果 / Execution results
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
# 确保工作目录存在 / Ensure working directory exists
|
|
131
|
+
Path(working_directory).mkdir(parents=True, exist_ok=True)
|
|
132
|
+
|
|
133
|
+
# 分割命令行 / Split command lines
|
|
134
|
+
command_lines = [
|
|
135
|
+
cmd.strip() for cmd in commands.strip().split("\n") if cmd.strip()
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
if not command_lines:
|
|
139
|
+
return [
|
|
140
|
+
types.TextContent(
|
|
141
|
+
type="text", text="没有提供有效命令 / No valid commands provided"
|
|
142
|
+
)
|
|
143
|
+
]
|
|
144
|
+
|
|
145
|
+
results = []
|
|
146
|
+
stats = {"successful": 0, "failed": 0, "timeout": 0}
|
|
147
|
+
|
|
148
|
+
for i, command in enumerate(command_lines, 1):
|
|
149
|
+
try:
|
|
150
|
+
# 执行命令 / Execute command
|
|
151
|
+
result = subprocess.run(
|
|
152
|
+
command,
|
|
153
|
+
shell=True,
|
|
154
|
+
cwd=working_directory,
|
|
155
|
+
capture_output=True,
|
|
156
|
+
text=True,
|
|
157
|
+
timeout=30, # 30秒超时
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if result.returncode == 0:
|
|
161
|
+
results.append(f"✅ Command {i}: {command}")
|
|
162
|
+
if result.stdout.strip():
|
|
163
|
+
results.append(f" 输出 / Output: {result.stdout.strip()}")
|
|
164
|
+
stats["successful"] += 1
|
|
165
|
+
else:
|
|
166
|
+
results.append(f"❌ Command {i}: {command}")
|
|
167
|
+
if result.stderr.strip():
|
|
168
|
+
results.append(f" 错误 / Error: {result.stderr.strip()}")
|
|
169
|
+
stats["failed"] += 1
|
|
170
|
+
|
|
171
|
+
except subprocess.TimeoutExpired:
|
|
172
|
+
results.append(f"⏱️ Command {i} 超时 / timeout: {command}")
|
|
173
|
+
stats["timeout"] += 1
|
|
174
|
+
except Exception as e:
|
|
175
|
+
results.append(f"💥 Command {i} 异常 / exception: {command} - {str(e)}")
|
|
176
|
+
stats["failed"] += 1
|
|
177
|
+
|
|
178
|
+
# 生成执行报告 / Generate execution report
|
|
179
|
+
summary = generate_execution_summary(working_directory, command_lines, stats)
|
|
180
|
+
final_result = summary + "\n" + "\n".join(results)
|
|
181
|
+
|
|
182
|
+
return [types.TextContent(type="text", text=final_result)]
|
|
183
|
+
|
|
184
|
+
except Exception as e:
|
|
185
|
+
return [
|
|
186
|
+
types.TextContent(
|
|
187
|
+
type="text",
|
|
188
|
+
text=f"批量命令执行失败 / Failed to execute command batch: {str(e)}",
|
|
189
|
+
)
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
async def execute_single_command(
|
|
194
|
+
command: str, working_directory: str
|
|
195
|
+
) -> list[types.TextContent]:
|
|
196
|
+
"""
|
|
197
|
+
执行单个shell命令 / Execute single shell command
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
command: 要执行的命令 / Command to execute
|
|
201
|
+
working_directory: 工作目录 / Working directory
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
执行结果 / Execution result
|
|
205
|
+
"""
|
|
206
|
+
try:
|
|
207
|
+
# 确保工作目录存在 / Ensure working directory exists
|
|
208
|
+
Path(working_directory).mkdir(parents=True, exist_ok=True)
|
|
209
|
+
|
|
210
|
+
# 执行命令 / Execute command
|
|
211
|
+
result = subprocess.run(
|
|
212
|
+
command,
|
|
213
|
+
shell=True,
|
|
214
|
+
cwd=working_directory,
|
|
215
|
+
capture_output=True,
|
|
216
|
+
text=True,
|
|
217
|
+
timeout=30,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
# 格式化输出 / Format output
|
|
221
|
+
output = format_single_command_result(command, working_directory, result)
|
|
222
|
+
|
|
223
|
+
return [types.TextContent(type="text", text=output)]
|
|
224
|
+
|
|
225
|
+
except subprocess.TimeoutExpired:
|
|
226
|
+
return [
|
|
227
|
+
types.TextContent(
|
|
228
|
+
type="text", text=f"⏱️ 命令超时 / Command timeout: {command}"
|
|
229
|
+
)
|
|
230
|
+
]
|
|
231
|
+
except Exception as e:
|
|
232
|
+
return [
|
|
233
|
+
types.TextContent(
|
|
234
|
+
type="text", text=f"💥 命令执行错误 / Command execution error: {str(e)}"
|
|
235
|
+
)
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def generate_execution_summary(
|
|
240
|
+
working_directory: str, command_lines: List[str], stats: Dict[str, int]
|
|
241
|
+
) -> str:
|
|
242
|
+
"""
|
|
243
|
+
生成执行总结 / Generate execution summary
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
working_directory: 工作目录 / Working directory
|
|
247
|
+
command_lines: 命令列表 / Command list
|
|
248
|
+
stats: 统计信息 / Statistics
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
格式化的总结 / Formatted summary
|
|
252
|
+
"""
|
|
253
|
+
return f"""
|
|
254
|
+
命令执行总结 / Command Execution Summary:
|
|
255
|
+
{'='*50}
|
|
256
|
+
工作目录 / Working Directory: {working_directory}
|
|
257
|
+
总命令数 / Total Commands: {len(command_lines)}
|
|
258
|
+
成功 / Successful: {stats['successful']}
|
|
259
|
+
失败 / Failed: {stats['failed']}
|
|
260
|
+
超时 / Timeout: {stats['timeout']}
|
|
261
|
+
|
|
262
|
+
详细结果 / Detailed Results:
|
|
263
|
+
{'-'*50}"""
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def format_single_command_result(
|
|
267
|
+
command: str, working_directory: str, result: subprocess.CompletedProcess
|
|
268
|
+
) -> str:
|
|
269
|
+
"""
|
|
270
|
+
格式化单命令执行结果 / Format single command execution result
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
command: 执行的命令 / Executed command
|
|
274
|
+
working_directory: 工作目录 / Working directory
|
|
275
|
+
result: 执行结果 / Execution result
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
格式化的结果 / Formatted result
|
|
279
|
+
"""
|
|
280
|
+
output = f"""
|
|
281
|
+
单命令执行 / Single Command Execution:
|
|
282
|
+
{'='*40}
|
|
283
|
+
工作目录 / Working Directory: {working_directory}
|
|
284
|
+
命令 / Command: {command}
|
|
285
|
+
返回码 / Return Code: {result.returncode}
|
|
286
|
+
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
if result.returncode == 0:
|
|
290
|
+
output += "✅ 状态 / Status: SUCCESS / 成功\n"
|
|
291
|
+
if result.stdout.strip():
|
|
292
|
+
output += f"输出 / Output:\n{result.stdout.strip()}\n"
|
|
293
|
+
else:
|
|
294
|
+
output += "❌ 状态 / Status: FAILED / 失败\n"
|
|
295
|
+
if result.stderr.strip():
|
|
296
|
+
output += f"错误 / Error:\n{result.stderr.strip()}\n"
|
|
297
|
+
|
|
298
|
+
return output
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
async def main():
|
|
302
|
+
"""
|
|
303
|
+
运行MCP服务器 / Run MCP server
|
|
304
|
+
"""
|
|
305
|
+
# 通过stdio运行服务器 / Run server via stdio
|
|
306
|
+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
|
307
|
+
await app.run(
|
|
308
|
+
read_stream,
|
|
309
|
+
write_stream,
|
|
310
|
+
InitializationOptions(
|
|
311
|
+
server_name="command-executor",
|
|
312
|
+
server_version="1.0.0",
|
|
313
|
+
capabilities=app.get_capabilities(
|
|
314
|
+
notification_options=NotificationOptions(),
|
|
315
|
+
experimental_capabilities={},
|
|
316
|
+
),
|
|
317
|
+
),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
if __name__ == "__main__":
|
|
322
|
+
import asyncio
|
|
323
|
+
|
|
324
|
+
asyncio.run(main())
|
tools/git_command.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
GitHub Repository Downloader MCP Tool using FastMCP
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
from typing import Dict, List, Optional
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from mcp.server import FastMCP
|
|
13
|
+
|
|
14
|
+
# 创建 FastMCP 实例
|
|
15
|
+
mcp = FastMCP("github-downloader")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GitHubURLExtractor:
|
|
19
|
+
"""提取GitHub URL的工具类"""
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def extract_github_urls(text: str) -> List[str]:
|
|
23
|
+
"""从文本中提取GitHub URLs"""
|
|
24
|
+
patterns = [
|
|
25
|
+
# 标准HTTPS URL
|
|
26
|
+
r"https?://github\.com/[\w\-\.]+/[\w\-\.]+(?:\.git)?",
|
|
27
|
+
# SSH URL
|
|
28
|
+
r"git@github\.com:[\w\-\.]+/[\w\-\.]+(?:\.git)?",
|
|
29
|
+
# 短格式 owner/repo - 更严格的匹配
|
|
30
|
+
r"(?<!\S)(?<!/)(?<!\.)([\w\-\.]+/[\w\-\.]+)(?!/)(?!\S)",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
urls = []
|
|
34
|
+
for pattern in patterns:
|
|
35
|
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
|
36
|
+
for match in matches:
|
|
37
|
+
# 处理短格式
|
|
38
|
+
if isinstance(match, tuple):
|
|
39
|
+
match = match[0]
|
|
40
|
+
|
|
41
|
+
# 清理URL
|
|
42
|
+
if match.startswith("git@"):
|
|
43
|
+
url = match.replace("git@github.com:", "https://github.com/")
|
|
44
|
+
elif match.startswith("http"):
|
|
45
|
+
url = match
|
|
46
|
+
else:
|
|
47
|
+
# 处理短格式 (owner/repo) - 添加更多验证
|
|
48
|
+
if "/" in match and not any(
|
|
49
|
+
x in match for x in ["./", "../", "deepcode_lab", "tools"]
|
|
50
|
+
):
|
|
51
|
+
parts = match.split("/")
|
|
52
|
+
if (
|
|
53
|
+
len(parts) == 2
|
|
54
|
+
and all(
|
|
55
|
+
part.replace("-", "").replace("_", "").isalnum()
|
|
56
|
+
for part in parts
|
|
57
|
+
)
|
|
58
|
+
and not any(part.startswith(".") for part in parts)
|
|
59
|
+
):
|
|
60
|
+
url = f"https://github.com/{match}"
|
|
61
|
+
else:
|
|
62
|
+
continue
|
|
63
|
+
else:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
# 规范化 URL
|
|
67
|
+
url = url.rstrip(".git")
|
|
68
|
+
url = url.rstrip("/")
|
|
69
|
+
|
|
70
|
+
# 修复重复的 github.com
|
|
71
|
+
if "github.com/github.com/" in url:
|
|
72
|
+
url = url.replace("github.com/github.com/", "github.com/")
|
|
73
|
+
|
|
74
|
+
urls.append(url)
|
|
75
|
+
|
|
76
|
+
return list(set(urls)) # 去重
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def extract_target_path(text: str) -> Optional[str]:
|
|
80
|
+
"""从文本中提取目标路径"""
|
|
81
|
+
# 路径指示词模式
|
|
82
|
+
patterns = [
|
|
83
|
+
r'(?:to|into|in|at)\s+(?:folder|directory|path)?\s*["\']?([^\s"\']+)["\']?',
|
|
84
|
+
r'(?:save|download|clone)\s+(?:to|into|at)\s+["\']?([^\s"\']+)["\']?',
|
|
85
|
+
# 中文支持
|
|
86
|
+
r'(?:到|在|保存到|下载到|克隆到)\s*["\']?([^\s"\']+)["\']?',
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
for pattern in patterns:
|
|
90
|
+
match = re.search(pattern, text, re.IGNORECASE)
|
|
91
|
+
if match:
|
|
92
|
+
path = match.group(1).strip("。,,.")
|
|
93
|
+
# 过滤掉通用词
|
|
94
|
+
if path and path.lower() not in [
|
|
95
|
+
"here",
|
|
96
|
+
"there",
|
|
97
|
+
"current",
|
|
98
|
+
"local",
|
|
99
|
+
"这里",
|
|
100
|
+
"当前",
|
|
101
|
+
"本地",
|
|
102
|
+
]:
|
|
103
|
+
return path
|
|
104
|
+
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def infer_repo_name(url: str) -> str:
|
|
109
|
+
"""从URL推断仓库名称"""
|
|
110
|
+
url = url.rstrip(".git")
|
|
111
|
+
if "github.com" in url:
|
|
112
|
+
parts = url.split("/")
|
|
113
|
+
if len(parts) >= 2:
|
|
114
|
+
return parts[-1]
|
|
115
|
+
return "repository"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
async def check_git_installed() -> bool:
|
|
119
|
+
"""检查Git是否安装"""
|
|
120
|
+
try:
|
|
121
|
+
proc = await asyncio.create_subprocess_exec(
|
|
122
|
+
"git",
|
|
123
|
+
"--version",
|
|
124
|
+
stdout=asyncio.subprocess.PIPE,
|
|
125
|
+
stderr=asyncio.subprocess.PIPE,
|
|
126
|
+
)
|
|
127
|
+
await proc.wait()
|
|
128
|
+
return proc.returncode == 0
|
|
129
|
+
except Exception:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def clone_repository(repo_url: str, target_path: str) -> Dict[str, any]:
|
|
134
|
+
"""执行git clone命令"""
|
|
135
|
+
try:
|
|
136
|
+
proc = await asyncio.create_subprocess_exec(
|
|
137
|
+
"git",
|
|
138
|
+
"clone",
|
|
139
|
+
repo_url,
|
|
140
|
+
target_path,
|
|
141
|
+
stdout=asyncio.subprocess.PIPE,
|
|
142
|
+
stderr=asyncio.subprocess.PIPE,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
stdout, stderr = await proc.communicate()
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
"success": proc.returncode == 0,
|
|
149
|
+
"stdout": stdout.decode("utf-8", errors="replace"),
|
|
150
|
+
"stderr": stderr.decode("utf-8", errors="replace"),
|
|
151
|
+
"returncode": proc.returncode,
|
|
152
|
+
}
|
|
153
|
+
except Exception as e:
|
|
154
|
+
return {"success": False, "error": str(e)}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@mcp.tool()
|
|
158
|
+
async def download_github_repo(instruction: str) -> str:
|
|
159
|
+
"""
|
|
160
|
+
Download GitHub repositories from natural language instructions.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
instruction: Natural language text containing GitHub URLs and optional target paths
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
Status message about the download operation
|
|
167
|
+
|
|
168
|
+
Examples:
|
|
169
|
+
- "Download https://github.com/openai/gpt-3"
|
|
170
|
+
- "Clone microsoft/vscode to my-projects folder"
|
|
171
|
+
- "Get https://github.com/facebook/react"
|
|
172
|
+
"""
|
|
173
|
+
# 检查Git是否安装
|
|
174
|
+
if not await check_git_installed():
|
|
175
|
+
return "❌ Error: Git is not installed or not in system PATH"
|
|
176
|
+
|
|
177
|
+
extractor = GitHubURLExtractor()
|
|
178
|
+
|
|
179
|
+
# 提取GitHub URLs
|
|
180
|
+
urls = extractor.extract_github_urls(instruction)
|
|
181
|
+
if not urls:
|
|
182
|
+
return "❌ No GitHub URLs found in the instruction"
|
|
183
|
+
|
|
184
|
+
# 提取目标路径
|
|
185
|
+
target_path = extractor.extract_target_path(instruction)
|
|
186
|
+
|
|
187
|
+
# 下载仓库
|
|
188
|
+
results = []
|
|
189
|
+
for url in urls:
|
|
190
|
+
try:
|
|
191
|
+
# 准备目标路径
|
|
192
|
+
if target_path:
|
|
193
|
+
# 判断是否为绝对路径
|
|
194
|
+
if os.path.isabs(target_path):
|
|
195
|
+
# 如果是绝对路径,直接使用
|
|
196
|
+
final_path = target_path
|
|
197
|
+
# 如果目标路径是目录,添加仓库名
|
|
198
|
+
if os.path.basename(target_path) == "" or target_path.endswith("/"):
|
|
199
|
+
final_path = os.path.join(
|
|
200
|
+
target_path, extractor.infer_repo_name(url)
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
# 如果是相对路径,保持相对路径
|
|
204
|
+
final_path = target_path
|
|
205
|
+
# 如果目标路径是目录,添加仓库名
|
|
206
|
+
if os.path.basename(target_path) == "" or target_path.endswith("/"):
|
|
207
|
+
final_path = os.path.join(
|
|
208
|
+
target_path, extractor.infer_repo_name(url)
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
final_path = extractor.infer_repo_name(url)
|
|
212
|
+
|
|
213
|
+
# 如果是相对路径,确保使用相对路径格式
|
|
214
|
+
if not os.path.isabs(final_path):
|
|
215
|
+
final_path = os.path.normpath(final_path)
|
|
216
|
+
if final_path.startswith("/"):
|
|
217
|
+
final_path = final_path.lstrip("/")
|
|
218
|
+
|
|
219
|
+
# 确保父目录存在
|
|
220
|
+
parent_dir = os.path.dirname(final_path)
|
|
221
|
+
if parent_dir:
|
|
222
|
+
os.makedirs(parent_dir, exist_ok=True)
|
|
223
|
+
|
|
224
|
+
# 检查目标路径是否已存在
|
|
225
|
+
if os.path.exists(final_path):
|
|
226
|
+
results.append(
|
|
227
|
+
f"❌ Failed to download {url}: Target path already exists: {final_path}"
|
|
228
|
+
)
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
# 执行克隆
|
|
232
|
+
result = await clone_repository(url, final_path)
|
|
233
|
+
|
|
234
|
+
if result["success"]:
|
|
235
|
+
msg = f"✅ Successfully downloaded: {url}\n"
|
|
236
|
+
msg += f" Location: {final_path}"
|
|
237
|
+
if result.get("stdout"):
|
|
238
|
+
msg += f"\n {result['stdout'].strip()}"
|
|
239
|
+
else:
|
|
240
|
+
msg = f"❌ Failed to download: {url}\n"
|
|
241
|
+
msg += f" Error: {result.get('error', result.get('stderr', 'Unknown error'))}"
|
|
242
|
+
|
|
243
|
+
except Exception as e:
|
|
244
|
+
msg = f"❌ Failed to download: {url}\n"
|
|
245
|
+
msg += f" Error: {str(e)}"
|
|
246
|
+
|
|
247
|
+
results.append(msg)
|
|
248
|
+
|
|
249
|
+
return "\n\n".join(results)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@mcp.tool()
|
|
253
|
+
async def parse_github_urls(text: str) -> str:
|
|
254
|
+
"""
|
|
255
|
+
Extract GitHub URLs and target paths from text.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
text: Text containing GitHub URLs
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
Parsed GitHub URLs and target path information
|
|
262
|
+
"""
|
|
263
|
+
extractor = GitHubURLExtractor()
|
|
264
|
+
|
|
265
|
+
urls = extractor.extract_github_urls(text)
|
|
266
|
+
target_path = extractor.extract_target_path(text)
|
|
267
|
+
|
|
268
|
+
content = "📝 Parsed information:\n\n"
|
|
269
|
+
|
|
270
|
+
if urls:
|
|
271
|
+
content += "GitHub URLs found:\n"
|
|
272
|
+
for url in urls:
|
|
273
|
+
content += f" • {url}\n"
|
|
274
|
+
else:
|
|
275
|
+
content += "No GitHub URLs found\n"
|
|
276
|
+
|
|
277
|
+
if target_path:
|
|
278
|
+
content += f"\nTarget path: {target_path}"
|
|
279
|
+
else:
|
|
280
|
+
content += "\nTarget path: Not specified (will use repository name)"
|
|
281
|
+
|
|
282
|
+
return content
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@mcp.tool()
|
|
286
|
+
async def git_clone(
|
|
287
|
+
repo_url: str, target_path: Optional[str] = None, branch: Optional[str] = None
|
|
288
|
+
) -> str:
|
|
289
|
+
"""
|
|
290
|
+
Clone a specific GitHub repository.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
repo_url: GitHub repository URL
|
|
294
|
+
target_path: Optional target directory path
|
|
295
|
+
branch: Optional branch name to clone
|
|
296
|
+
|
|
297
|
+
Returns:
|
|
298
|
+
Status message about the clone operation
|
|
299
|
+
"""
|
|
300
|
+
# 检查Git是否安装
|
|
301
|
+
if not await check_git_installed():
|
|
302
|
+
return "❌ Error: Git is not installed or not in system PATH"
|
|
303
|
+
|
|
304
|
+
# 准备目标路径
|
|
305
|
+
if not target_path:
|
|
306
|
+
extractor = GitHubURLExtractor()
|
|
307
|
+
target_path = extractor.infer_repo_name(repo_url)
|
|
308
|
+
|
|
309
|
+
# 转换为绝对路径
|
|
310
|
+
if not os.path.isabs(target_path):
|
|
311
|
+
target_path = str(Path.cwd() / target_path)
|
|
312
|
+
|
|
313
|
+
# 检查目标路径
|
|
314
|
+
if os.path.exists(target_path):
|
|
315
|
+
return f"❌ Error: Target path already exists: {target_path}"
|
|
316
|
+
|
|
317
|
+
# 构建命令
|
|
318
|
+
cmd = ["git", "clone"]
|
|
319
|
+
if branch:
|
|
320
|
+
cmd.extend(["-b", branch])
|
|
321
|
+
cmd.extend([repo_url, target_path])
|
|
322
|
+
|
|
323
|
+
# 执行克隆
|
|
324
|
+
try:
|
|
325
|
+
proc = await asyncio.create_subprocess_exec(
|
|
326
|
+
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
stdout, stderr = await proc.communicate()
|
|
330
|
+
|
|
331
|
+
if proc.returncode == 0:
|
|
332
|
+
result = "✅ Successfully cloned repository\n"
|
|
333
|
+
result += f"Repository: {repo_url}\n"
|
|
334
|
+
result += f"Location: {target_path}"
|
|
335
|
+
if branch:
|
|
336
|
+
result += f"\nBranch: {branch}"
|
|
337
|
+
return result
|
|
338
|
+
else:
|
|
339
|
+
return f"❌ Clone failed\nError: {stderr.decode('utf-8', errors='replace')}"
|
|
340
|
+
|
|
341
|
+
except Exception as e:
|
|
342
|
+
return f"❌ Clone failed\nError: {str(e)}"
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# 主程序入口
|
|
346
|
+
if __name__ == "__main__":
|
|
347
|
+
print("🚀 GitHub Repository Downloader MCP Tool")
|
|
348
|
+
print("📝 Starting server with FastMCP...")
|
|
349
|
+
print("\nAvailable tools:")
|
|
350
|
+
print(" • download_github_repo - Download repos from natural language")
|
|
351
|
+
print(" • parse_github_urls - Extract GitHub URLs from text")
|
|
352
|
+
print(" • git_clone - Clone a specific repository")
|
|
353
|
+
print("")
|
|
354
|
+
|
|
355
|
+
# 运行服务器
|
|
356
|
+
mcp.run()
|