StarrySkyWorld commited on
Commit
022f7b7
·
verified ·
1 Parent(s): cd541db

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +581 -153
main.py CHANGED
@@ -1,10 +1,14 @@
1
  #!/usr/bin/env python3
2
  """
3
- MCP Server Control Plugin - 基于 Streamable HTTP 的服务器远程操控插件
4
- 功能:执行命令、读写文件、上传下载文件、目录操作、文件分享等
5
-
6
- 依赖安装:
7
- pip install "fastmcp>=2.3.1" fastapi python-multipart uvicorn
 
 
 
 
8
  """
9
 
10
  import os
@@ -17,14 +21,15 @@ import stat
17
  import asyncio
18
  import secrets
19
  import mimetypes
 
 
20
  from pathlib import Path
21
- from typing import Optional, List, Dict, Any
22
  from datetime import datetime, timedelta
23
  from dataclasses import dataclass
24
 
25
- # 改这一行
26
- from fastmcp import FastMCP # 不是 from mcp.server.fastmcp
27
- from fastapi import FastAPI, UploadFile, File, Query, Request
28
  from fastapi.responses import JSONResponse, FileResponse
29
  import uvicorn
30
 
@@ -34,17 +39,19 @@ PORT = 8000
34
  SERVER_NAME = "Server Control MCP"
35
  DEFAULT_UPLOAD_DIR = "/tmp/mcp_uploads"
36
  MAX_UPLOAD_SIZE = 500 * 1024 * 1024 # 500MB
37
- # HuggingFace 会设置 BASE_URL,本地默认为 localhost
 
 
 
38
  BASE_URL = os.getenv("BASE_URL", f"http://localhost:{PORT}").rstrip('/')
39
 
40
  # 确保上传目录存在
41
  Path(DEFAULT_UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
42
 
43
- # ============ 文件分享数据结构 ============
44
 
45
  @dataclass
46
  class FileShare:
47
- """文件分享记录"""
48
  id: str
49
  source_path: str
50
  filename: str
@@ -53,11 +60,20 @@ class FileShare:
53
  download_count: int = 0
54
  max_downloads: Optional[int] = None
55
 
 
 
 
 
 
 
 
 
56
  # 全局存储
57
  file_shares: Dict[str, FileShare] = {}
58
 
 
 
59
  def generate_short_id(length: int = 6) -> str:
60
- """生成短ID"""
61
  alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
62
  while True:
63
  new_id = ''.join(secrets.choice(alphabet) for _ in range(length))
@@ -65,224 +81,636 @@ def generate_short_id(length: int = 6) -> str:
65
  return new_id
66
 
67
  def cleanup_expired_shares():
68
- """清理过期的分享"""
69
  now = datetime.now()
70
- expired = [
71
- sid for sid, share in file_shares.items()
72
- if share.expires_at and share.expires_at < now
73
- ]
74
  for sid in expired:
75
  del file_shares[sid]
76
- return len(expired)
77
 
78
- # ============ 创建 MCP 服务器实例 ============
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
- mcp = FastMCP(
81
- name=SERVER_NAME,
82
- )
 
 
83
 
84
- # ============ MCP 工具: 命令执行 ============
 
 
85
 
86
  @mcp.tool()
87
- async def execute_command(
88
- command: str,
89
- working_dir: Optional[str] = None,
90
- timeout: int = 300,
91
- shell: bool = True
 
92
  ) -> Dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  """执行系统命令"""
94
  try:
95
  cwd = working_dir or os.getcwd()
96
- process = await asyncio.create_subprocess_shell(
97
- command,
98
- stdout=asyncio.subprocess.PIPE,
99
- stderr=asyncio.subprocess.PIPE,
100
- cwd=cwd
101
- ) if shell else await asyncio.create_subprocess_exec(
102
- *command.split(),
103
- stdout=asyncio.subprocess.PIPE,
104
- stderr=asyncio.subprocess.PIPE,
105
- cwd=cwd
106
  )
107
- stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout)
108
  return {
109
- "success": True,
110
  "stdout": stdout.decode('utf-8', errors='replace'),
111
  "stderr": stderr.decode('utf-8', errors='replace'),
112
- "return_code": process.returncode,
113
- "command": command
114
  }
115
- except Exception as e:
116
- return {"success": False, "error": str(e)}
117
 
118
  @mcp.tool()
119
- async def execute_script(
120
- script_content: str,
121
- interpreter: str = "/bin/bash",
122
- working_dir: Optional[str] = None
123
- ) -> Dict[str, Any]:
124
- """执行脚本内容"""
125
- import tempfile
126
  try:
127
- with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f:
128
- f.write(script_content)
129
- script_path = f.name
130
- os.chmod(script_path, 0o755)
131
- result = await execute_command(f"{interpreter} {script_path}", working_dir=working_dir)
132
- os.unlink(script_path)
133
- return result
134
- except Exception as e:
135
- return {"success": False, "error": str(e)}
136
 
137
- # ============ MCP 工具: 文件操作 ============
138
 
139
  @mcp.tool()
140
  def read_file(path: str, encoding: str = "utf-8", binary: bool = False) -> Dict[str, Any]:
141
- """读取文件内容"""
142
  try:
143
- file_path = Path(path).expanduser().resolve()
144
  if binary:
145
- with open(file_path, 'rb') as f:
146
- content = base64.b64encode(f.read()).decode('ascii')
147
- return {"success": True, "content": content, "encoding": "base64", "size": file_path.stat().st_size}
148
- else:
149
- with open(file_path, 'r', encoding=encoding) as f:
150
- content = f.read()
151
- return {"success": True, "content": content, "size": len(content)}
152
- except Exception as e:
153
- return {"success": False, "error": str(e)}
154
 
155
  @mcp.tool()
156
  def write_file(path: str, content: str, binary: bool = False, append: bool = False) -> Dict[str, Any]:
157
- """写入文件内容"""
158
  try:
159
- file_path = Path(path).expanduser().resolve()
160
- file_path.parent.mkdir(parents=True, exist_ok=True)
161
  mode = ('ab' if append else 'wb') if binary else ('a' if append else 'w')
162
- if binary:
163
- with open(file_path, mode) as f: f.write(base64.b64decode(content))
164
- else:
165
- with open(file_path, mode, encoding='utf-8') as f: f.write(content)
166
- return {"success": True, "path": str(file_path), "size": file_path.stat().st_size}
167
- except Exception as e:
168
- return {"success": False, "error": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
  @mcp.tool()
171
  def list_directory(path: str = ".", recursive: bool = False) -> Dict[str, Any]:
172
- """列出目录内容"""
173
  try:
174
  p = Path(path).expanduser().resolve()
175
  items = []
176
  for item in (p.rglob('*') if recursive else p.iterdir()):
177
- items.append({
178
- "name": item.name,
179
- "path": str(item),
180
- "type": "dir" if item.is_dir() else "file",
181
- "size": item.stat().st_size if item.is_file() else None
182
- })
183
  return {"success": True, "items": items}
184
- except Exception as e:
185
- return {"success": False, "error": str(e)}
 
 
 
 
 
 
 
186
 
187
- # ============ MCP 工具: 文件分享 & 上传 ============
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
  @mcp.tool()
190
  def share_file(path: str, filename: Optional[str] = None, expires_in: int = 3600) -> Dict[str, Any]:
191
- """分享文件,生成用户下载链接"""
192
  try:
193
- file_path = Path(path).expanduser().resolve()
194
- if not file_path.is_file(): return {"success": False, "error": "Not a file"}
195
  cleanup_expired_shares()
196
- share_id = generate_short_id()
197
- share = FileShare(
198
- id=share_id, source_path=str(file_path),
199
- filename=filename or file_path.name,
200
- created_at=datetime.now(),
201
- expires_at=datetime.now() + timedelta(seconds=expires_in)
202
- )
203
- file_shares[share_id] = share
204
- return {"success": True, "url": f"{BASE_URL}/f/{share_id}", "expires_at": share.expires_at.isoformat()}
205
- except Exception as e:
206
- return {"success": False, "error": str(e)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
 
208
  @mcp.tool()
209
  def upload_to_server(content: str, filename: str, directory: str = DEFAULT_UPLOAD_DIR) -> Dict[str, Any]:
210
- """小文件上传 (base64)"""
211
  return write_file(f"{directory}/{filename}", content, binary=True)
212
 
213
  @mcp.tool()
214
  def get_upload_endpoint() -> Dict[str, Any]:
215
- """获取 HTTP 大文件上传端点信息"""
216
  return {
217
- "url": f"{BASE_URL}/upload",
218
- "method": "POST",
219
- "field": "file",
220
  "example": f"curl -F 'file=@file.zip' '{BASE_URL}/upload?dir={DEFAULT_UPLOAD_DIR}'"
221
  }
222
 
223
- # ============ 系统信息工具 ============
224
 
225
  @mcp.tool()
226
  def get_system_info() -> Dict[str, Any]:
227
- """获取系统状态"""
228
  import platform
229
- return {
230
- "platform": platform.system(),
231
- "cpu": platform.machine(),
232
- "mem": str(shutil.disk_usage('/')),
233
- "time": datetime.now().isoformat()
234
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
 
236
  # ============ FastAPI 应用构建 ============
237
 
238
- def create_combined_app():
239
- # 1. 获取 MCP ASGI 应用
240
  mcp_app = mcp.http_app(path="/mcp")
241
-
242
- # 2. 创建 FastAPI,并继承 MCP 的生命周期
243
  app = FastAPI(title=SERVER_NAME, lifespan=mcp_app.lifespan)
244
 
245
  @app.get("/")
246
  async def index():
247
- return {"server": SERVER_NAME, "mcp": "/mcp/", "upload": "/upload"}
248
-
249
- @app.get("/f/{share_id}")
250
- async def download(share_id: str):
251
- if share_id not in file_shares:
252
- return JSONResponse({"error": "Expired or not found"}, status_code=404)
253
- share = file_shares[share_id]
254
- if share.expires_at and datetime.now() > share.expires_at:
255
- return JSONResponse({"error": "Expired"}, status_code=410)
256
- share.download_count += 1
257
- return FileResponse(share.source_path, filename=share.filename)
 
 
 
 
 
258
 
259
  @app.post("/upload")
260
  async def upload(
261
- file: UploadFile = File(...),
262
  dir: str = Query(DEFAULT_UPLOAD_DIR),
263
  overwrite: bool = Query(False)
264
  ):
265
  try:
266
- target_dir = Path(dir).expanduser().resolve()
267
- target_dir.mkdir(parents=True, exist_ok=True)
268
- file_path = target_dir / file.filename
269
- if file_path.exists() and not overwrite:
270
- return JSONResponse({"error": "Exists"}, status_code=409)
271
-
272
- with open(file_path, "wb") as f:
273
- f.write(await file.read())
274
- return {"success": True, "path": str(file_path), "size": file_path.stat().st_size}
275
- except Exception as e:
276
- return JSONResponse({"error": str(e)}, status_code=500)
277
 
278
- # 3. 挂载 MCP
279
  app.mount("/", mcp_app)
280
-
281
  return app
282
 
283
- # ============ 启动 ============
284
-
285
  if __name__ == "__main__":
286
- combined_app = create_combined_app()
287
  print(f"Starting {SERVER_NAME} on {BASE_URL}")
288
- uvicorn.run(combined_app, host=HOST, port=PORT)
 
1
  #!/usr/bin/env python3
2
  """
3
+ MCP Server Control Plugin - 全功能版
4
+ 功能:
5
+ 1. 系统控制:命令执行、服务管理、进程查看
6
+ 2. 文件操作:读写、复制移动、权限管理
7
+ 3. 精准编辑:行号编辑、正则替换、Diff/Patch 应用 (新增)
8
+ 4. 文件服务:HTTP 上传/下载、临时文件分享
9
+ 5. 信息查询:系统信息、网络状态、搜索
10
+
11
+ 依赖: pip install "fastmcp>=2.3.1" fastapi python-multipart uvicorn
12
  """
13
 
14
  import os
 
21
  import asyncio
22
  import secrets
23
  import mimetypes
24
+ import re
25
+ import tempfile
26
  from pathlib import Path
27
+ from typing import Optional, List, Dict, Any, Tuple
28
  from datetime import datetime, timedelta
29
  from dataclasses import dataclass
30
 
31
+ from fastmcp import FastMCP
32
+ from fastapi import FastAPI, UploadFile, File, Query
 
33
  from fastapi.responses import JSONResponse, FileResponse
34
  import uvicorn
35
 
 
39
  SERVER_NAME = "Server Control MCP"
40
  DEFAULT_UPLOAD_DIR = "/tmp/mcp_uploads"
41
  MAX_UPLOAD_SIZE = 500 * 1024 * 1024 # 500MB
42
+ MAX_EDIT_FILE_SIZE = 10 * 1024 * 1024 # 10MB (精准编辑限制)
43
+ MAX_MATCHES_RETURN = 50 # 搜索替换最大返回数
44
+
45
+ # HuggingFace Spaces 或其他环境的 URL 配置
46
  BASE_URL = os.getenv("BASE_URL", f"http://localhost:{PORT}").rstrip('/')
47
 
48
  # 确保上传目录存在
49
  Path(DEFAULT_UPLOAD_DIR).mkdir(parents=True, exist_ok=True)
50
 
51
+ # ============ 数据结构 ============
52
 
53
  @dataclass
54
  class FileShare:
 
55
  id: str
56
  source_path: str
57
  filename: str
 
60
  download_count: int = 0
61
  max_downloads: Optional[int] = None
62
 
63
+ @dataclass
64
+ class DiffHunk:
65
+ old_start: int
66
+ old_count: int
67
+ new_start: int
68
+ new_count: int
69
+ lines: List[Tuple[str, str]]
70
+
71
  # 全局存储
72
  file_shares: Dict[str, FileShare] = {}
73
 
74
+ # ============ 辅助函数: 文件分享 ============
75
+
76
  def generate_short_id(length: int = 6) -> str:
 
77
  alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
78
  while True:
79
  new_id = ''.join(secrets.choice(alphabet) for _ in range(length))
 
81
  return new_id
82
 
83
  def cleanup_expired_shares():
 
84
  now = datetime.now()
85
+ expired = [sid for sid, s in file_shares.items() if s.expires_at and s.expires_at < now]
 
 
 
86
  for sid in expired:
87
  del file_shares[sid]
 
88
 
89
+ # ============ 辅助函数: 精准编辑 ============
90
+
91
+ def safe_read_file(path: str) -> Tuple[bool, str, List[str]]:
92
+ """安全读取文件,自动处理编码"""
93
+ file_path = Path(path).expanduser().resolve()
94
+ if not file_path.exists(): return False, f"File not found: {path}", []
95
+ if not file_path.is_file(): return False, f"Not a file: {path}", []
96
+
97
+ size = file_path.stat().st_size
98
+ if size > MAX_EDIT_FILE_SIZE:
99
+ return False, f"File too large: {size} bytes (max {MAX_EDIT_FILE_SIZE})", []
100
+
101
+ encodings = ['utf-8', 'utf-8-sig', 'latin-1', 'cp1252']
102
+ for encoding in encodings:
103
+ try:
104
+ with open(file_path, 'r', encoding=encoding) as f:
105
+ content = f.read()
106
+ if '\x00' in content: return False, "Binary file detected", []
107
+ lines = content.splitlines(keepends=True)
108
+ if lines and not lines[-1].endswith('\n'): lines[-1] += '\n'
109
+ return True, encoding, lines
110
+ except UnicodeDecodeError:
111
+ continue
112
+ return False, "Unable to decode file", []
113
+
114
+ def safe_write_file(path: str, lines: List[str], encoding: str = 'utf-8') -> Tuple[bool, str]:
115
+ """安全写入文件(原子写入)"""
116
+ file_path = Path(path).expanduser().resolve()
117
+ try:
118
+ dir_path = file_path.parent
119
+ with tempfile.NamedTemporaryFile(mode='w', encoding=encoding, dir=dir_path, delete=False, suffix='.tmp') as tmp:
120
+ tmp.writelines(lines)
121
+ tmp_path = tmp.name
122
+ shutil.move(tmp_path, file_path)
123
+ return True, ""
124
+ except Exception as e:
125
+ if 'tmp_path' in locals(): Path(tmp_path).unlink(missing_ok=True)
126
+ return False, str(e)
127
+
128
+ def create_backup(path: str) -> Tuple[bool, str]:
129
+ """创建备份文件"""
130
+ file_path = Path(path).expanduser().resolve()
131
+ backup_path = file_path.with_suffix(file_path.suffix + '.bak')
132
+ try:
133
+ shutil.copy2(file_path, backup_path)
134
+ return True, str(backup_path)
135
+ except Exception as e:
136
+ return False, str(e)
137
+
138
+ def normalize_line_content(content: str) -> List[str]:
139
+ if not content: return []
140
+ lines = content.splitlines(keepends=True)
141
+ if lines and not lines[-1].endswith('\n'): lines[-1] += '\n'
142
+ return lines
143
+
144
+ def parse_unified_diff(diff_text: str) -> Tuple[bool, str, List[DiffHunk]]:
145
+ """解析 Unified Diff"""
146
+ lines = diff_text.strip().split('\n')
147
+ hunks = []
148
+ i = 0
149
+ # Skip headers
150
+ while i < len(lines) and not lines[i].startswith('@@'): i += 1
151
+
152
+ while i < len(lines):
153
+ line = lines[i]
154
+ m = re.match(r'^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@', line)
155
+ if not m:
156
+ i += 1
157
+ continue
158
+
159
+ old_start, old_count = int(m.group(1)), int(m.group(2) or 1)
160
+ new_start, new_count = int(m.group(3)), int(m.group(4) or 1)
161
+ i += 1
162
+
163
+ hunk_lines = []
164
+ while i < len(lines):
165
+ if lines[i].startswith('@@'): break
166
+ hl = lines[i]
167
+ if not hl: hunk_lines.append((' ', ''))
168
+ elif hl[0] in (' ', '-', '+'): hunk_lines.append((hl[0], hl[1:]))
169
+ elif hl[0] == '\\': pass
170
+ else: hunk_lines.append((' ', hl))
171
+ i += 1
172
+ hunks.append(DiffHunk(old_start, old_count, new_start, new_count, hunk_lines))
173
+
174
+ if not hunks: return False, "No valid hunks found", []
175
+ return True, "", hunks
176
+
177
+ def apply_hunk(lines: List[str], hunk: DiffHunk, offset: int) -> Tuple[bool, str, List[str], int]:
178
+ """应用单个 Hunk"""
179
+ start_idx = hunk.old_start - 1 + offset
180
+ if start_idx < 0: return False, f"Invalid position: {start_idx}", lines, offset
181
+
182
+ # 验证 Context
183
+ check_idx = start_idx
184
+ for type, expected in hunk.lines:
185
+ if type in (' ', '-'):
186
+ if check_idx >= len(lines): return False, "Hunk extends beyond file", lines, offset
187
+ if lines[check_idx].rstrip('\n\r') != expected.rstrip('\n\r'):
188
+ return False, f"Context mismatch at line {check_idx+1}", lines, offset
189
+ check_idx += 1
190
+
191
+ # 应用修改
192
+ new_lines = lines[:start_idx]
193
+ read_idx = start_idx
194
+ for type, content in hunk.lines:
195
+ if type == ' ':
196
+ new_lines.append(lines[read_idx])
197
+ read_idx += 1
198
+ elif type == '-':
199
+ read_idx += 1
200
+ elif type == '+':
201
+ new_lines.append(content + '\n' if not content.endswith('\n') else content)
202
+ new_lines.extend(lines[read_idx:])
203
+
204
+ removed = sum(1 for t, _ in hunk.lines if t == '-')
205
+ added = sum(1 for t, _ in hunk.lines if t == '+')
206
+ return True, "", new_lines, offset + added - removed
207
+
208
+ # ============ MCP 实例 ============
209
+
210
+ mcp = FastMCP(name=SERVER_NAME)
211
+
212
+ # ============ MCP 工具: 精准编辑 (New) ============
213
+
214
+ @mcp.tool()
215
+ def edit_file_lines(
216
+ path: str,
217
+ start_line: int,
218
+ end_line: Optional[int] = None,
219
+ content: str = "",
220
+ action: str = "replace",
221
+ backup: bool = False,
222
+ dry_run: bool = False
223
+ ) -> Dict[str, Any]:
224
+ """
225
+ 按行号精准编辑文件
226
+ Args:
227
+ action: "replace" | "insert_before" | "insert_after" | "delete"
228
+ start_line: 起始行 (1-based)
229
+ end_line: 结束行 (包含)
230
+ backup: 是否备份
231
+ dry_run: 仅预览
232
+ """
233
+ if action not in ["replace", "insert_before", "insert_after", "delete"]:
234
+ return {"success": False, "error": f"Invalid action: {action}"}
235
+ if start_line < 1: return {"success": False, "error": "start_line must be >= 1"}
236
+ if end_line is None: end_line = start_line
237
+ if end_line < start_line: return {"success": False, "error": "end_line must be >= start_line"}
238
+
239
+ success, enc_or_err, lines = safe_read_file(path)
240
+ if not success: return {"success": False, "error": enc_or_err}
241
+
242
+ total = len(lines)
243
+ # 验证范围
244
+ if action in ["replace", "delete"] and (start_line > total or end_line > total):
245
+ return {"success": False, "error": "Line range exceeds file length"}
246
+
247
+ work_lines = lines.copy()
248
+ new_content = normalize_line_content(content) if content else []
249
+
250
+ if action == "replace":
251
+ work_lines[start_line-1:end_line] = new_content
252
+ removed, added = end_line - start_line + 1, len(new_content)
253
+ elif action == "insert_before":
254
+ for i, l in enumerate(new_content): work_lines.insert(start_line-1+i, l)
255
+ removed, added = 0, len(new_content)
256
+ elif action == "insert_after":
257
+ for i, l in enumerate(new_content): work_lines.insert(start_line+i, l)
258
+ removed, added = 0, len(new_content)
259
+ elif action == "delete":
260
+ del work_lines[start_line-1:end_line]
261
+ removed, added = end_line - start_line + 1, 0
262
+
263
+ if dry_run:
264
+ return {"success": True, "dry_run": True, "action": action, "lines_removed": removed, "lines_added": added}
265
+
266
+ backup_path = None
267
+ if backup:
268
+ s, r = create_backup(path)
269
+ if not s: return {"success": False, "error": f"Backup failed: {r}"}
270
+ backup_path = r
271
+
272
+ s, e = safe_write_file(path, work_lines, enc_or_err)
273
+ if not s: return {"success": False, "error": e}
274
+
275
+ return {"success": True, "action": action, "lines_removed": removed, "lines_added": added, "backup_path": backup_path}
276
+
277
+ @mcp.tool()
278
+ def search_replace_file(
279
+ path: str,
280
+ search: str,
281
+ replace: str,
282
+ regex: bool = False,
283
+ count: int = 0,
284
+ ignore_case: bool = False,
285
+ whole_word: bool = False,
286
+ backup: bool = False,
287
+ dry_run: bool = False
288
+ ) -> Dict[str, Any]:
289
+ """搜索并替换文件内容 (支持正则)"""
290
+ if not search: return {"success": False, "error": "Search pattern empty"}
291
+ flags = re.IGNORECASE if ignore_case else 0
292
+ try:
293
+ if regex: pattern = re.compile(search, flags)
294
+ elif whole_word: pattern = re.compile(r'\b' + re.escape(search) + r'\b', flags)
295
+ else: pattern = re.compile(re.escape(search), flags)
296
+ except re.error as e: return {"success": False, "error": str(e)}
297
+
298
+ success, enc_or_err, lines = safe_read_file(path)
299
+ if not success: return {"success": False, "error": enc_or_err}
300
+
301
+ matches, new_lines, total_replaced = [], [], 0
302
+ max_count = count if count > 0 else float('inf')
303
+
304
+ for i, line in enumerate(lines, 1):
305
+ if total_replaced >= max_count:
306
+ new_lines.append(line)
307
+ continue
308
+
309
+ found = list(pattern.finditer(line))
310
+ if not found:
311
+ new_lines.append(line)
312
+ continue
313
+
314
+ remaining = int(max_count - total_replaced)
315
+ if len(found) <= remaining:
316
+ new_line = pattern.sub(replace, line)
317
+ replaced = len(found)
318
+ else:
319
+ new_line = pattern.sub(replace, line, count=remaining)
320
+ replaced = remaining
321
+
322
+ total_replaced += replaced
323
+ new_lines.append(new_line)
324
+ if len(matches) < MAX_MATCHES_RETURN:
325
+ matches.append({"line": i, "before": line.strip(), "after": new_line.strip()})
326
+
327
+ if total_replaced == 0: return {"success": True, "replacements": 0, "message": "No matches"}
328
+ if dry_run: return {"success": True, "dry_run": True, "replacements": total_replaced, "matches": matches}
329
 
330
+ backup_path = None
331
+ if backup:
332
+ s, r = create_backup(path)
333
+ if not s: return {"success": False, "error": f"Backup failed: {r}"}
334
+ backup_path = r
335
 
336
+ s, e = safe_write_file(path, new_lines, enc_or_err)
337
+ if not s: return {"success": False, "error": e}
338
+ return {"success": True, "replacements": total_replaced, "matches": matches, "backup_path": backup_path}
339
 
340
  @mcp.tool()
341
+ def apply_diff(
342
+ path: str,
343
+ diff: str,
344
+ backup: bool = False,
345
+ dry_run: bool = False,
346
+ reverse: bool = False
347
  ) -> Dict[str, Any]:
348
+ """应用 Unified Diff 补丁"""
349
+ if not diff.strip(): return {"success": False, "error": "Empty diff"}
350
+
351
+ s, err, hunks = parse_unified_diff(diff)
352
+ if not s: return {"success": False, "error": err}
353
+
354
+ if reverse:
355
+ rev_hunks = []
356
+ for h in hunks:
357
+ rev_lines = []
358
+ for t, c in h.lines:
359
+ if t == '+': rev_lines.append(('-', c))
360
+ elif t == '-': rev_lines.append(('+', c))
361
+ else: rev_lines.append((t, c))
362
+ rev_hunks.append(DiffHunk(h.new_start, h.new_count, h.old_start, h.old_count, rev_lines))
363
+ hunks = rev_hunks
364
+
365
+ success, enc_or_err, lines = safe_read_file(path)
366
+ if not success: return {"success": False, "error": enc_or_err}
367
+
368
+ offset, details, added, removed = 0, [], 0, 0
369
+ for i, hunk in enumerate(hunks):
370
+ s, err, lines, offset = apply_hunk(lines, hunk, offset)
371
+ if not s: return {"success": False, "error": f"Hunk #{i+1}: {err}"}
372
+ h_rem = sum(1 for t, _ in hunk.lines if t == '-')
373
+ h_add = sum(1 for t, _ in hunk.lines if t == '+')
374
+ removed += h_rem
375
+ added += h_add
376
+ details.append({"hunk": i+1, "removed": h_rem, "added": h_add})
377
+
378
+ if dry_run:
379
+ return {"success": True, "dry_run": True, "hunks": len(hunks), "added": added, "removed": removed}
380
+
381
+ backup_path = None
382
+ if backup:
383
+ s, r = create_backup(path)
384
+ if not s: return {"success": False, "error": f"Backup failed: {r}"}
385
+ backup_path = r
386
+
387
+ s, e = safe_write_file(path, lines, enc_or_err)
388
+ if not s: return {"success": False, "error": e}
389
+ return {"success": True, "hunks": len(hunks), "added": added, "removed": removed, "backup_path": backup_path}
390
+
391
+ # ============ MCP 工具: 命令与系统 ============
392
+
393
+ @mcp.tool()
394
+ async def execute_command(command: str, working_dir: Optional[str] = None, timeout: int = 300) -> Dict[str, Any]:
395
  """执行系统命令"""
396
  try:
397
  cwd = working_dir or os.getcwd()
398
+ proc = await asyncio.create_subprocess_shell(
399
+ command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, cwd=cwd
 
 
 
 
 
 
 
 
400
  )
401
+ stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout)
402
  return {
403
+ "success": True,
404
  "stdout": stdout.decode('utf-8', errors='replace'),
405
  "stderr": stderr.decode('utf-8', errors='replace'),
406
+ "return_code": proc.returncode
 
407
  }
408
+ except Exception as e: return {"success": False, "error": str(e)}
 
409
 
410
  @mcp.tool()
411
+ async def execute_script(script_content: str, interpreter: str = "/bin/bash", working_dir: Optional[str] = None) -> Dict[str, Any]:
412
+ """执行脚本"""
413
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.sh', delete=False) as f:
414
+ f.write(script_content)
415
+ path = f.name
416
+ os.chmod(path, 0o755)
 
417
  try:
418
+ return await execute_command(f"{interpreter} {path}", working_dir)
419
+ finally:
420
+ os.unlink(path)
 
 
 
 
 
 
421
 
422
+ # ============ MCP 工具: 基础文件操作 ============
423
 
424
  @mcp.tool()
425
  def read_file(path: str, encoding: str = "utf-8", binary: bool = False) -> Dict[str, Any]:
426
+ """读取文件"""
427
  try:
428
+ p = Path(path).expanduser().resolve()
429
  if binary:
430
+ with open(p, 'rb') as f: return {"success": True, "content": base64.b64encode(f.read()).decode('ascii'), "encoding": "base64"}
431
+ with open(p, 'r', encoding=encoding) as f: return {"success": True, "content": f.read()}
432
+ except Exception as e: return {"success": False, "error": str(e)}
 
 
 
 
 
 
433
 
434
  @mcp.tool()
435
  def write_file(path: str, content: str, binary: bool = False, append: bool = False) -> Dict[str, Any]:
436
+ """写入文件"""
437
  try:
438
+ p = Path(path).expanduser().resolve()
439
+ p.parent.mkdir(parents=True, exist_ok=True)
440
  mode = ('ab' if append else 'wb') if binary else ('a' if append else 'w')
441
+ data = base64.b64decode(content) if binary else content
442
+ encoding = None if binary else 'utf-8'
443
+ with open(p, mode, encoding=encoding) as f: f.write(data)
444
+ return {"success": True, "path": str(p), "size": p.stat().st_size}
445
+ except Exception as e: return {"success": False, "error": str(e)}
446
+
447
+ @mcp.tool()
448
+ def delete_file(path: str, force: bool = False) -> Dict[str, Any]:
449
+ """删除文件或目录"""
450
+ try:
451
+ p = Path(path).expanduser().resolve()
452
+ if p.is_file(): p.unlink()
453
+ elif p.is_dir(): shutil.rmtree(p) if force else p.rmdir()
454
+ else: return {"success": False, "error": "Not found"}
455
+ return {"success": True, "path": str(p)}
456
+ except Exception as e: return {"success": False, "error": str(e)}
457
+
458
+ @mcp.tool()
459
+ def copy_path(source: str, destination: str, overwrite: bool = False) -> Dict[str, Any]:
460
+ """复制"""
461
+ try:
462
+ src, dst = Path(source).expanduser().resolve(), Path(destination).expanduser().resolve()
463
+ if not src.exists(): return {"success": False, "error": "Source not found"}
464
+ if dst.exists() and not overwrite: return {"success": False, "error": "Destination exists"}
465
+ if src.is_file(): shutil.copy2(src, dst)
466
+ else: shutil.copytree(src, dst, dirs_exist_ok=overwrite)
467
+ return {"success": True}
468
+ except Exception as e: return {"success": False, "error": str(e)}
469
+
470
+ @mcp.tool()
471
+ def move_path(source: str, destination: str) -> Dict[str, Any]:
472
+ """移动"""
473
+ try:
474
+ src, dst = Path(source).expanduser().resolve(), Path(destination).expanduser().resolve()
475
+ shutil.move(str(src), str(dst))
476
+ return {"success": True}
477
+ except Exception as e: return {"success": False, "error": str(e)}
478
+
479
+ @mcp.tool()
480
+ def upload_file(path: str, content_base64: str, create_dirs: bool = True) -> Dict[str, Any]:
481
+ """上传文件 (Base64)"""
482
+ return write_file(path, content_base64, binary=True)
483
+
484
+ @mcp.tool()
485
+ def download_file(path: str) -> Dict[str, Any]:
486
+ """下载文件 (Base64)"""
487
+ return read_file(path, binary=True)
488
+
489
+ # ============ MCP 工具: 目录与搜索 ============
490
 
491
  @mcp.tool()
492
  def list_directory(path: str = ".", recursive: bool = False) -> Dict[str, Any]:
493
+ """列出目录"""
494
  try:
495
  p = Path(path).expanduser().resolve()
496
  items = []
497
  for item in (p.rglob('*') if recursive else p.iterdir()):
498
+ items.append({"name": item.name, "path": str(item), "type": "dir" if item.is_dir() else "file"})
 
 
 
 
 
499
  return {"success": True, "items": items}
500
+ except Exception as e: return {"success": False, "error": str(e)}
501
+
502
+ @mcp.tool()
503
+ def create_directory(path: str) -> Dict[str, Any]:
504
+ """创建目录"""
505
+ try:
506
+ Path(path).expanduser().resolve().mkdir(parents=True, exist_ok=True)
507
+ return {"success": True}
508
+ except Exception as e: return {"success": False, "error": str(e)}
509
 
510
+ @mcp.tool()
511
+ def search_files(directory: str, pattern: str, max_results: int = 100) -> Dict[str, Any]:
512
+ """文件名搜索"""
513
+ try:
514
+ p = Path(directory).expanduser().resolve()
515
+ results = [str(f) for i, f in enumerate(p.rglob(pattern)) if i < max_results]
516
+ return {"success": True, "results": results}
517
+ except Exception as e: return {"success": False, "error": str(e)}
518
+
519
+ @mcp.tool()
520
+ def search_in_files(directory: str, search_text: str, file_pattern: str = "*", max_results: int = 50) -> Dict[str, Any]:
521
+ """文件内容搜索"""
522
+ try:
523
+ p = Path(directory).expanduser().resolve()
524
+ results = []
525
+ for f in p.rglob(file_pattern):
526
+ if not f.is_file(): continue
527
+ try:
528
+ with open(f, 'r', errors='ignore') as fp:
529
+ for i, line in enumerate(fp, 1):
530
+ if search_text in line:
531
+ results.append({"file": str(f), "line": i, "content": line.strip()})
532
+ if len(results) >= max_results: return {"success": True, "results": results, "truncated": True}
533
+ except: continue
534
+ return {"success": True, "results": results}
535
+ except Exception as e: return {"success": False, "error": str(e)}
536
+
537
+ # ============ MCP 工具: 分享与上传 ============
538
 
539
  @mcp.tool()
540
  def share_file(path: str, filename: Optional[str] = None, expires_in: int = 3600) -> Dict[str, Any]:
541
+ """生成下载链接"""
542
  try:
543
+ p = Path(path).expanduser().resolve()
544
+ if not p.is_file(): return {"success": False, "error": "Not a file"}
545
  cleanup_expired_shares()
546
+ sid = generate_short_id()
547
+ share = FileShare(sid, str(p), filename or p.name, datetime.now(), datetime.now() + timedelta(seconds=expires_in))
548
+ file_shares[sid] = share
549
+ return {"success": True, "url": f"{BASE_URL}/f/{sid}", "expires_at": share.expires_at.isoformat()}
550
+ except Exception as e: return {"success": False, "error": str(e)}
551
+
552
+ @mcp.tool()
553
+ def list_shares() -> Dict[str, Any]:
554
+ """列出分享"""
555
+ return {"success": True, "shares": [{"id": s.id, "url": f"{BASE_URL}/f/{s.id}", "file": s.filename} for s in file_shares.values()]}
556
+
557
+ @mcp.tool()
558
+ def revoke_share(share_id: str) -> Dict[str, Any]:
559
+ """撤销分享"""
560
+ if share_id in file_shares:
561
+ del file_shares[share_id]
562
+ return {"success": True}
563
+ return {"success": False, "error": "Share not found"}
564
+
565
+ @mcp.tool()
566
+ def get_share_info(share_id: str) -> Dict[str, Any]:
567
+ """获取分享详情"""
568
+ if share_id not in file_shares: return {"success": False, "error": "Share not found"}
569
+ s = file_shares[share_id]
570
+ return {
571
+ "success": True, "id": s.id, "url": f"{BASE_URL}/f/{s.id}",
572
+ "filename": s.filename, "downloads": s.download_count,
573
+ "expires_at": s.expires_at.isoformat() if s.expires_at else None
574
+ }
575
 
576
  @mcp.tool()
577
  def upload_to_server(content: str, filename: str, directory: str = DEFAULT_UPLOAD_DIR) -> Dict[str, Any]:
578
+ """小文件上传 (Base64)"""
579
  return write_file(f"{directory}/{filename}", content, binary=True)
580
 
581
  @mcp.tool()
582
  def get_upload_endpoint() -> Dict[str, Any]:
583
+ """获取 HTTP 上传端点"""
584
  return {
585
+ "success": True, "url": f"{BASE_URL}/upload", "method": "POST", "field": "file",
 
 
586
  "example": f"curl -F 'file=@file.zip' '{BASE_URL}/upload?dir={DEFAULT_UPLOAD_DIR}'"
587
  }
588
 
589
+ # ============ MCP 工具: 系统信息与管理 ============
590
 
591
  @mcp.tool()
592
  def get_system_info() -> Dict[str, Any]:
593
+ """获取系统信息"""
594
  import platform
595
+ try:
596
+ disk = shutil.disk_usage('/')
597
+ return {
598
+ "platform": platform.platform(),
599
+ "python": platform.python_version(),
600
+ "cpu": platform.machine(),
601
+ "disk_total_gb": round(disk.total / (1024**3), 2),
602
+ "disk_free_gb": round(disk.free / (1024**3), 2),
603
+ "cwd": os.getcwd()
604
+ }
605
+ except Exception as e: return {"success": False, "error": str(e)}
606
+
607
+ @mcp.tool()
608
+ def get_process_list(filter_name: Optional[str] = None) -> Dict[str, Any]:
609
+ """获取进程列表"""
610
+ cmd = "ps aux" + (f" | grep -i {filter_name}" if filter_name else "")
611
+ try:
612
+ res = subprocess.run(cmd, shell=True, capture_output=True, text=True)
613
+ return {"success": True, "output": res.stdout[:2000] + ("..." if len(res.stdout)>2000 else "")}
614
+ except Exception as e: return {"success": False, "error": str(e)}
615
+
616
+ @mcp.tool()
617
+ def get_network_info() -> Dict[str, Any]:
618
+ """获取网络信息"""
619
+ try:
620
+ res = subprocess.run("ip a", shell=True, capture_output=True, text=True)
621
+ return {"success": True, "output": res.stdout}
622
+ except Exception as e: return {"success": False, "error": str(e)}
623
+
624
+ @mcp.tool()
625
+ async def check_port(host: str, port: int) -> Dict[str, Any]:
626
+ """检查端口"""
627
+ try:
628
+ _, w = await asyncio.wait_for(asyncio.open_connection(host, port), timeout=3)
629
+ w.close()
630
+ return {"success": True, "status": "open"}
631
+ except: return {"success": False, "status": "closed/timeout"}
632
+
633
+ @mcp.tool()
634
+ def get_environment_variables(prefix: Optional[str] = None) -> Dict[str, Any]:
635
+ """获取环境变量"""
636
+ env = os.environ.copy()
637
+ if prefix: env = {k: v for k, v in env.items() if k.startswith(prefix)}
638
+ return {"success": True, "env": env}
639
+
640
+ @mcp.tool()
641
+ def change_permissions(path: str, mode: str, recursive: bool = False) -> Dict[str, Any]:
642
+ """修改权限 (chmod)"""
643
+ try:
644
+ p = Path(path).expanduser().resolve()
645
+ m = int(mode, 8)
646
+ if recursive and p.is_dir():
647
+ for item in p.rglob('*'): os.chmod(item, m)
648
+ os.chmod(p, m)
649
+ return {"success": True}
650
+ except Exception as e: return {"success": False, "error": str(e)}
651
+
652
+ @mcp.tool()
653
+ def get_file_info(path: str) -> Dict[str, Any]:
654
+ """获取文件详情"""
655
+ try:
656
+ s = Path(path).expanduser().resolve().stat()
657
+ return {
658
+ "success": True, "size": s.st_size, "mode": oct(s.st_mode),
659
+ "uid": s.st_uid, "gid": s.st_gid, "mtime": datetime.fromtimestamp(s.st_mtime).isoformat()
660
+ }
661
+ except Exception as e: return {"success": False, "error": str(e)}
662
+
663
+ @mcp.tool()
664
+ async def service_control(service: str, action: str) -> Dict[str, Any]:
665
+ """服务控制 (systemctl)"""
666
+ if action not in ["start", "stop", "restart", "status"]: return {"error": "Invalid action"}
667
+ return await execute_command(f"systemctl {action} {service}")
668
 
669
  # ============ FastAPI 应用构建 ============
670
 
671
+ def create_app():
 
672
  mcp_app = mcp.http_app(path="/mcp")
 
 
673
  app = FastAPI(title=SERVER_NAME, lifespan=mcp_app.lifespan)
674
 
675
  @app.get("/")
676
  async def index():
677
+ return {
678
+ "server": SERVER_NAME,
679
+ "endpoints": {
680
+ "mcp": f"{BASE_URL}/mcp/",
681
+ "upload": f"{BASE_URL}/upload",
682
+ "download": f"{BASE_URL}/f/{{id}}"
683
+ }
684
+ }
685
+
686
+ @app.get("/f/{sid}")
687
+ async def download(sid: str):
688
+ if sid not in file_shares: return JSONResponse({"error": "Not found"}, 404)
689
+ s = file_shares[sid]
690
+ if s.expires_at and datetime.now() > s.expires_at: return JSONResponse({"error": "Expired"}, 410)
691
+ s.download_count += 1
692
+ return FileResponse(s.source_path, filename=s.filename)
693
 
694
  @app.post("/upload")
695
  async def upload(
696
+ file: UploadFile = File(...),
697
  dir: str = Query(DEFAULT_UPLOAD_DIR),
698
  overwrite: bool = Query(False)
699
  ):
700
  try:
701
+ d = Path(dir).expanduser().resolve()
702
+ d.mkdir(parents=True, exist_ok=True)
703
+ p = d / file.filename
704
+ if p.exists() and not overwrite: return JSONResponse({"error": "Exists"}, 409)
705
+ content = await file.read()
706
+ if len(content) > MAX_UPLOAD_SIZE: return JSONResponse({"error": "Too large"}, 413)
707
+ with open(p, "wb") as f: f.write(content)
708
+ return {"success": True, "path": str(p), "size": len(content)}
709
+ except Exception as e: return JSONResponse({"error": str(e)}, 500)
 
 
710
 
 
711
  app.mount("/", mcp_app)
 
712
  return app
713
 
 
 
714
  if __name__ == "__main__":
 
715
  print(f"Starting {SERVER_NAME} on {BASE_URL}")
716
+ uvicorn.run(create_app(), host=HOST, port=PORT)