Coverage for agentos/mcp/builtin_servers.py: 28%

400 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-06 10:59 +0800

1""" 

2Built-in MCP Servers for AgentOS (v1.8.1). 

3 

4Pure Python implementations of common MCP tools. 8 servers, 32+ tools total. 

5 

6Servers: 

7 FilesystemServer (7) - Safe file I/O with path validation 

8 WebFetchServer (3) - HTTP client with content extraction 

9 MemoryServer (6) - Persistent knowledge graph 

10 SearchServer (4) - Web search via DuckDuckGo 

11 GitServer (4) - Git operations 

12 ShellServer (3) - Safe shell command execution 

13 CodeServer (3) - Python/JS code execution in sandbox 

14 TextServer (4) - Text manipulation & formatting 

15""" 

16 

17from __future__ import annotations 

18 

19import json 

20import os 

21import re 

22import hashlib 

23import shutil 

24import subprocess 

25import time 

26import urllib.parse 

27import urllib.request 

28import urllib.error 

29from datetime import datetime 

30from pathlib import Path 

31from typing import Any, Dict, List, Optional 

32 

33 

34# ── Filesystem MCP Server (7 tools) ───────── 

35 

36 

37class FilesystemServer: 

38 """MCP-compatible filesystem server with safe path validation.""" 

39 

40 NAME = "filesystem" 

41 VERSION = "1.0.0" 

42 

43 def __init__(self, allowed_paths: Optional[List[str]] = None): 

44 self._allowed_paths = [ 

45 Path(p).resolve() 

46 for p in (allowed_paths or [os.getcwd(), str(Path.home())]) 

47 ] 

48 for p in self._allowed_paths: 

49 p.mkdir(parents=True, exist_ok=True) 

50 

51 def _validate_path(self, path_str: str) -> Path: 

52 p = Path(path_str).expanduser().resolve() 

53 for allowed in self._allowed_paths: 

54 try: 

55 p.relative_to(allowed) 

56 return p 

57 except ValueError: 

58 continue 

59 raise ValueError(f"Path '{path_str}' outside allowed directories") 

60 

61 def get_tools(self) -> List[Dict[str, Any]]: 

62 return [ 

63 {"name": "read_file", "description": "Read contents of a text file", 

64 "inputSchema": {"type": "object", "properties": { 

65 "path": {"type": "string"}, "encoding": {"type": "string", "default": "utf-8"}}, 

66 "required": ["path"]}}, 

67 {"name": "write_file", "description": "Write text content to a file", 

68 "inputSchema": {"type": "object", "properties": { 

69 "path": {"type": "string"}, "content": {"type": "string"}, 

70 "encoding": {"type": "string", "default": "utf-8"}}, 

71 "required": ["path", "content"]}}, 

72 {"name": "list_directory", "description": "List directory contents with metadata", 

73 "inputSchema": {"type": "object", "properties": { 

74 "path": {"type": "string"}, "recursive": {"type": "boolean", "default": False}}, 

75 "required": ["path"]}}, 

76 {"name": "search_files", "description": "Search files by glob pattern", 

77 "inputSchema": {"type": "object", "properties": { 

78 "path": {"type": "string"}, "pattern": {"type": "string"}}, 

79 "required": ["path", "pattern"]}}, 

80 {"name": "get_file_info", "description": "Get file/directory metadata", 

81 "inputSchema": {"type": "object", "properties": { 

82 "path": {"type": "string"}}, "required": ["path"]}}, 

83 {"name": "create_directory", "description": "Create directory and parents", 

84 "inputSchema": {"type": "object", "properties": { 

85 "path": {"type": "string"}}, "required": ["path"]}}, 

86 {"name": "move_file", "description": "Move or rename a file/directory", 

87 "inputSchema": {"type": "object", "properties": { 

88 "source": {"type": "string"}, "destination": {"type": "string"}}, 

89 "required": ["source", "destination"]}}, 

90 ] 

91 

92 def call_tool(self, tool_name: str, arguments: Dict) -> Any: 

93 return getattr(self, f"_handle_{tool_name}")(**arguments) 

94 

95 def _handle_read_file(self, path: str, encoding: str = "utf-8") -> str: 

96 p = self._validate_path(path) 

97 if not p.is_file(): raise FileNotFoundError(f"Not found: {p}") 

98 return p.read_text(encoding=encoding) 

99 

100 def _handle_write_file(self, path: str, content: str, encoding: str = "utf-8") -> str: 

101 p = self._validate_path(path) 

102 p.parent.mkdir(parents=True, exist_ok=True) 

103 p.write_text(content, encoding=encoding) 

104 return f"Wrote {len(content)} bytes to {p}" 

105 

106 def _handle_list_directory(self, path: str, recursive: bool = False) -> List[Dict]: 

107 p = self._validate_path(path) 

108 if not p.is_dir(): raise NotADirectoryError(f"Not a directory: {p}") 

109 entries = [] 

110 for item in (p.rglob("*") if recursive else p.iterdir()): 

111 if item.name.startswith("."): continue 

112 s = item.stat() 

113 entries.append({"name": item.name, "path": str(item), "size": s.st_size, 

114 "is_dir": item.is_dir(), "modified": datetime.fromtimestamp(s.st_mtime).isoformat()}) 

115 return sorted(entries, key=lambda e: (not e["is_dir"], e["name"])) 

116 

117 def _handle_search_files(self, path: str, pattern: str) -> List[Dict]: 

118 p = self._validate_path(path) 

119 if not p.is_dir(): raise NotADirectoryError(f"Not a directory: {p}") 

120 return sorted([{"name": i.name, "path": str(i), "size": i.stat().st_size, "is_dir": i.is_dir()} 

121 for i in p.rglob(pattern) if not i.name.startswith(".")], key=lambda e: e["path"]) 

122 

123 def _handle_get_file_info(self, path: str) -> Dict: 

124 p = self._validate_path(path) 

125 if not p.exists(): raise FileNotFoundError(f"Not found: {p}") 

126 s = p.stat(); ext = p.suffix.lower() 

127 mime = {".txt": "text/plain", ".md": "text/markdown", ".py": "text/x-python", 

128 ".json": "application/json", ".html": "text/html", ".pdf": "application/pdf"} 

129 return {"name": p.name, "path": str(p), "size": s.st_size, "is_dir": p.is_dir(), 

130 "is_file": p.is_file(), "extension": ext, "mime_type": mime.get(ext, "application/octet-stream"), 

131 "created": datetime.fromtimestamp(s.st_ctime).isoformat(), 

132 "modified": datetime.fromtimestamp(s.st_mtime).isoformat(), 

133 "permissions": oct(s.st_mode)[-3:]} 

134 

135 def _handle_create_directory(self, path: str) -> str: 

136 p = self._validate_path(path) 

137 p.mkdir(parents=True, exist_ok=True) 

138 return f"Created: {p}" 

139 

140 def _handle_move_file(self, source: str, destination: str) -> str: 

141 src = self._validate_path(source); dst = self._validate_path(destination) 

142 if not src.exists(): raise FileNotFoundError(f"Not found: {src}") 

143 src.rename(dst) 

144 return f"Moved {src} -> {dst}" 

145 

146 

147# ── Web Fetch MCP Server (3 tools) ─────────── 

148 

149 

150class WebFetchServer: 

151 """MCP-compatible HTTP client with content extraction.""" 

152 

153 NAME = "webfetch" 

154 VERSION = "1.0.0" 

155 

156 def __init__(self, user_agent: str = "AgentOS-MCP/1.0", timeout: int = 30): 

157 self._ua = user_agent; self._timeout = timeout 

158 

159 def get_tools(self) -> List[Dict[str, Any]]: 

160 return [ 

161 {"name": "fetch_url", "description": "Fetch a web page and return cleaned text", 

162 "inputSchema": {"type": "object", "properties": { 

163 "url": {"type": "string"}, "max_length": {"type": "integer", "default": 50000}}, 

164 "required": ["url"]}}, 

165 {"name": "fetch_json", "description": "Fetch and parse JSON from URL", 

166 "inputSchema": {"type": "object", "properties": { 

167 "url": {"type": "string"}, "headers": {"type": "object"}}, 

168 "required": ["url"]}}, 

169 {"name": "check_url", "description": "HEAD request to verify URL accessibility", 

170 "inputSchema": {"type": "object", "properties": { 

171 "url": {"type": "string"}}, "required": ["url"]}}, 

172 ] 

173 

174 def call_tool(self, tool_name: str, arguments: Dict) -> Any: 

175 return getattr(self, f"_handle_{tool_name}")(**arguments) 

176 

177 @staticmethod 

178 def _validate_url(url: str) -> None: 

179 p = urllib.parse.urlparse(url) 

180 if p.scheme not in ("http", "https"): raise ValueError(f"Invalid scheme: {p.scheme}") 

181 

182 @staticmethod 

183 def _strip_html(html: str) -> str: 

184 html = re.sub(r'<script[^>]*>.*?</script>', '', html, flags=re.DOTALL | re.I) 

185 html = re.sub(r'<style[^>]*>.*?</style>', '', html, flags=re.DOTALL | re.I) 

186 text = re.sub(r'<[^>]+>', ' ', html) 

187 text = re.sub(r'\s+', ' ', text).strip() 

188 for e, c in [('&amp;','&'),('&lt;','<'),('&gt;','>'),('&quot;','"'),('&#39;',"'"),('&nbsp;',' ')]: 

189 text = text.replace(e, c) 

190 return text 

191 

192 def _handle_fetch_url(self, url: str, max_length: int = 50000) -> str: 

193 self._validate_url(url) 

194 req = urllib.request.Request(url, headers={"User-Agent": self._ua}) 

195 try: 

196 with urllib.request.urlopen(req, timeout=self._timeout) as r: 

197 ct = r.headers.get("Content-Type", "") 

198 enc = ct.split("charset=")[-1].split(";")[0].strip() if "charset=" in ct else "utf-8" 

199 txt = r.read().decode(enc, errors="replace") 

200 if "text/html" in ct: txt = self._strip_html(txt) 

201 return txt[:max_length] + (f"\n\n[Truncated at {max_length}]" if len(txt) > max_length else "") 

202 except urllib.error.HTTPError as e: return f"HTTP {e.code}: {e.reason}" 

203 except Exception as e: return f"Error: {e}" 

204 

205 def _handle_fetch_json(self, url: str, headers: Optional[Dict] = None) -> Any: 

206 self._validate_url(url) 

207 h = {"User-Agent": self._ua, "Accept": "application/json"} 

208 if headers: h.update(headers) 

209 try: 

210 with urllib.request.urlopen(urllib.request.Request(url, headers=h), timeout=self._timeout) as r: 

211 return json.loads(r.read()) 

212 except json.JSONDecodeError as e: return {"error": "Invalid JSON", "detail": str(e)} 

213 except Exception as e: return {"error": str(e)} 

214 

215 def _handle_check_url(self, url: str) -> Dict: 

216 self._validate_url(url) 

217 req = urllib.request.Request(url, headers={"User-Agent": self._ua}, method="HEAD") 

218 try: 

219 with urllib.request.urlopen(req, timeout=self._timeout) as r: 

220 return {"url": url, "status": r.status, "accessible": 200 <= r.status < 400, 

221 "content_type": r.headers.get("Content-Type",""), "content_length": r.headers.get("Content-Length","")} 

222 except urllib.error.HTTPError as e: return {"url": url, "status": e.code, "accessible": False, "reason": e.reason} 

223 except Exception as e: return {"url": url, "status": 0, "accessible": False, "error": str(e)} 

224 

225 

226# ── Memory / Knowledge Graph MCP (6 tools) ─── 

227 

228 

229class MemoryServer: 

230 """Persistent knowledge graph for agent memory.""" 

231 

232 NAME = "memory" 

233 VERSION = "1.0.0" 

234 

235 def __init__(self, storage_path: str = ""): 

236 self._path = Path(storage_path or str(Path.home() / ".agentos" / "memory" / "kg.json")) 

237 self._path.parent.mkdir(parents=True, exist_ok=True) 

238 self._entries: Dict[str, Dict] = {} 

239 if self._path.exists(): 

240 try: 

241 for e in json.loads(self._path.read_text()).get("entries", []): self._entries[e["id"]] = e 

242 except: pass 

243 

244 def _save(self): 

245 self._path.write_text(json.dumps({"version": "1.0", "updated_at": time.time(), 

246 "entries": list(self._entries.values())}, indent=2, ensure_ascii=False)) 

247 

248 def _make_id(self, content: str) -> str: 

249 return hashlib.sha256(content.encode()).hexdigest()[:16] 

250 

251 def get_tools(self) -> List[Dict[str, Any]]: 

252 return [ 

253 {"name": "store_memory", "description": "Store a fact/memory", 

254 "inputSchema": {"type": "object", "properties": { 

255 "content": {"type": "string"}, "category": {"type": "string", "default": "general"}, 

256 "tags": {"type": "array", "items": {"type": "string"}}, "metadata": {"type": "object"}}, 

257 "required": ["content"]}}, 

258 {"name": "retrieve_memory", "description": "Retrieve memory by ID", 

259 "inputSchema": {"type": "object", "properties": {"memory_id": {"type": "string"}}, "required": ["memory_id"]}}, 

260 {"name": "search_memory", "description": "Search memories by keyword/category/tags", 

261 "inputSchema": {"type": "object", "properties": { 

262 "query": {"type": "string"}, "category": {"type": "string"}, 

263 "tags": {"type": "array", "items": {"type": "string"}}, "limit": {"type": "integer", "default": 20}}}}, 

264 {"name": "list_categories", "description": "List all categories with counts", 

265 "inputSchema": {"type": "object", "properties": {}}}, 

266 {"name": "delete_memory", "description": "Delete a memory by ID", 

267 "inputSchema": {"type": "object", "properties": {"memory_id": {"type": "string"}}, "required": ["memory_id"]}}, 

268 {"name": "update_memory", "description": "Update memory content/metadata", 

269 "inputSchema": {"type": "object", "properties": { 

270 "memory_id": {"type": "string"}, "content": {"type": "string"}, 

271 "category": {"type": "string"}, "tags": {"type": "array", "items": {"type": "string"}}}, 

272 "required": ["memory_id"]}}, 

273 ] 

274 

275 def call_tool(self, tool_name: str, arguments: Dict) -> Any: 

276 return getattr(self, f"_handle_{tool_name}")(**arguments) 

277 

278 def _handle_store_memory(self, content: str, category: str = "general", 

279 tags: list = None, metadata: dict = None) -> Dict: 

280 mid = self._make_id(content) 

281 entry = self._entries.get(mid, {}) 

282 if entry: 

283 entry["updated_at"] = time.time() 

284 if tags: entry.setdefault("tags", []).extend(t for t in tags if t not in entry.get("tags", [])) 

285 if category != "general": entry["category"] = category 

286 self._save(); return {"id": mid, "action": "updated"} 

287 entry = {"id": mid, "content": content, "category": category, "tags": tags or [], 

288 "metadata": metadata or {}, "created_at": time.time(), "updated_at": time.time()} 

289 self._entries[mid] = entry; self._save() 

290 return {"id": mid, "action": "stored"} 

291 

292 def _handle_retrieve_memory(self, memory_id: str) -> Optional[Dict]: 

293 return self._entries.get(memory_id) 

294 

295 def _handle_search_memory(self, query: str = "", category: str = None, 

296 tags: list = None, limit: int = 20) -> List[Dict]: 

297 results = [] 

298 for e in self._entries.values(): 

299 if category and e.get("category") != category: continue 

300 if tags and not all(t in e.get("tags", []) for t in tags): continue 

301 if query and query.lower() not in e.get("content","").lower(): continue 

302 results.append({"id": e["id"], "content": e["content"], "category": e.get("category",""), 

303 "tags": e.get("tags",[]), "created_at": e.get("created_at",0)}) 

304 return sorted(results, key=lambda r: r["created_at"], reverse=True)[:limit] 

305 

306 def _handle_list_categories(self) -> Dict[str, int]: 

307 c: Dict[str, int] = {} 

308 for e in self._entries.values(): c[e.get("category","general")] = c.get(e.get("category","general"), 0) + 1 

309 return dict(sorted(c.items(), key=lambda x: -x[1])) 

310 

311 def _handle_delete_memory(self, memory_id: str) -> Dict: 

312 if memory_id in self._entries: del self._entries[memory_id]; self._save(); return {"deleted": True, "id": memory_id} 

313 return {"deleted": False, "id": memory_id, "reason": "not found"} 

314 

315 def _handle_update_memory(self, memory_id: str, content: str = None, category: str = None, tags: list = None) -> Dict: 

316 e = self._entries.get(memory_id) 

317 if not e: return {"updated": False, "reason": "not found"} 

318 if content is not None: e["content"] = content 

319 if category is not None: e["category"] = category 

320 if tags is not None: e["tags"] = tags 

321 e["updated_at"] = time.time(); self._save() 

322 return {"updated": True, "id": memory_id} 

323 

324 

325# ── Web Search Server (4 tools) ────────────── 

326 

327 

328class SearchServer: 

329 """MCP-compatible web search via DuckDuckGo + Google fallback.""" 

330 

331 NAME = "search" 

332 VERSION = "1.0.0" 

333 

334 def get_tools(self) -> List[Dict[str, Any]]: 

335 return [ 

336 {"name": "web_search", "description": "Search the web", 

337 "inputSchema": {"type": "object", "properties": { 

338 "query": {"type": "string"}, "max_results": {"type": "integer", "default": 10}}, 

339 "required": ["query"]}}, 

340 {"name": "news_search", "description": "Search for recent news", 

341 "inputSchema": {"type": "object", "properties": { 

342 "query": {"type": "string"}, "max_results": {"type": "integer", "default": 10}}, 

343 "required": ["query"]}}, 

344 {"name": "image_search", "description": "Search for images", 

345 "inputSchema": {"type": "object", "properties": { 

346 "query": {"type": "string"}, "max_results": {"type": "integer", "default": 10}}, 

347 "required": ["query"]}}, 

348 {"name": "suggest", "description": "Get search autocomplete suggestions", 

349 "inputSchema": {"type": "object", "properties": { 

350 "query": {"type": "string"}}, "required": ["query"]}}, 

351 ] 

352 

353 def call_tool(self, tool_name: str, arguments: Dict) -> Any: 

354 return getattr(self, f"_handle_{tool_name}")(**arguments) 

355 

356 def _handle_web_search(self, query: str, max_results: int = 10) -> List[Dict]: 

357 """Search via DuckDuckGo HTML (no API key needed).""" 

358 q = urllib.parse.quote_plus(query) 

359 url = f"https://html.duckduckgo.com/html/?q={q}" 

360 req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) 

361 try: 

362 with urllib.request.urlopen(req, timeout=15) as r: 

363 html = r.read().decode("utf-8", errors="replace") 

364 except Exception as e: 

365 return [{"error": f"Search failed: {e}"}] 

366 

367 results = [] 

368 # Parse DuckDuckGo HTML results 

369 for m in re.finditer(r'<a[^>]*class="result__a"[^>]*href="([^"]*)"[^>]*>(.*?)</a>', html, re.DOTALL): 

370 if len(results) >= max_results: break 

371 link = m.group(1) 

372 title = re.sub(r'<[^>]+>', '', m.group(2)).strip() 

373 # Find snippet 

374 snippet = "" 

375 sn_match = re.search(r'<a[^>]*class="result__snippet"[^>]*>(.*?)</a>', html[m.end():m.end()+500], re.DOTALL) 

376 if sn_match: snippet = re.sub(r'<[^>]+>', '', sn_match.group(1)).strip() 

377 results.append({"title": title, "url": link, "snippet": snippet, "source": "duckduckgo"}) 

378 return results 

379 

380 def _handle_news_search(self, query: str, max_results: int = 10) -> List[Dict]: 

381 q = urllib.parse.quote_plus(f"{query} news") 

382 return self._handle_web_search(q, max_results) 

383 

384 def _handle_image_search(self, query: str, max_results: int = 10) -> List[Dict]: 

385 q = urllib.parse.quote_plus(query) 

386 url = f"https://duckduckgo.com/?q={q}&iax=images&ia=images" 

387 req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) 

388 try: 

389 with urllib.request.urlopen(req, timeout=15) as r: 

390 html = r.read().decode("utf-8", errors="replace") 

391 except Exception as e: 

392 return [{"error": str(e)}] 

393 

394 # Extract vqd token for image search 

395 vqd = "" 

396 vqd_m = re.search(r'vqd=([\d-]+)', html) 

397 if vqd_m: vqd = vqd_m.group(1) 

398 

399 if vqd: 

400 img_url = f"https://duckduckgo.com/i.js?q={q}&vqd={vqd}&o=json&p=1&s=0" 

401 try: 

402 with urllib.request.urlopen(urllib.request.Request(img_url, headers={"User-Agent": "Mozilla/5.0"}), timeout=15) as r: 

403 data = json.loads(r.read()) 

404 results = data.get("results", [])[:max_results] 

405 return [{"title": r.get("title",""), "url": r.get("url",""), 

406 "thumbnail": r.get("thumbnail",""), "source": "duckduckgo"} for r in results] 

407 except: pass 

408 return [] 

409 

410 def _handle_suggest(self, query: str) -> List[str]: 

411 q = urllib.parse.quote_plus(query) 

412 url = f"https://duckduckgo.com/ac/?q={q}&type=list" 

413 try: 

414 with urllib.request.urlopen(url, timeout=8) as r: 

415 data = json.loads(r.read()) 

416 return [item.get("phrase","") for item in data[:10]] 

417 except: return [] 

418 

419 

420# ── Git Server (4 tools) ───────────────────── 

421 

422 

423class GitServer: 

424 """MCP-compatible git operations.""" 

425 

426 NAME = "git" 

427 VERSION = "1.0.0" 

428 

429 def __init__(self, repo_path: str = ""): 

430 self._repo_path = Path(repo_path).resolve() if repo_path else Path.cwd() 

431 self._git: Optional[str] = shutil.which("git") 

432 

433 def _run_git(self, *args) -> Dict[str, Any]: 

434 if not self._git: return {"error": "git not installed"} 

435 try: 

436 r = subprocess.run([self._git, *args], capture_output=True, text=True, 

437 cwd=str(self._repo_path), timeout=30) 

438 return {"stdout": r.stdout.strip(), "stderr": r.stderr.strip(), "returncode": r.returncode} 

439 except subprocess.TimeoutExpired: 

440 return {"error": "Command timed out"} 

441 except Exception as e: 

442 return {"error": str(e)} 

443 

444 def get_tools(self) -> List[Dict[str, Any]]: 

445 return [ 

446 {"name": "git_status", "description": "Show working tree status", 

447 "inputSchema": {"type": "object", "properties": {}}}, 

448 {"name": "git_log", "description": "Show commit history", 

449 "inputSchema": {"type": "object", "properties": { 

450 "max_count": {"type": "integer", "default": 10}, "oneline": {"type": "boolean", "default": True}}}}, 

451 {"name": "git_diff", "description": "Show changes between commits/working tree", 

452 "inputSchema": {"type": "object", "properties": { 

453 "staged": {"type": "boolean", "default": False}, "commit": {"type": "string"}}}}, 

454 {"name": "git_branch", "description": "List branches", 

455 "inputSchema": {"type": "object", "properties": {"remote": {"type": "boolean", "default": False}}}}, 

456 ] 

457 

458 def call_tool(self, tool_name: str, arguments: Dict) -> Any: 

459 return getattr(self, f"_handle_{tool_name}")(**arguments) 

460 

461 def _handle_git_status(self) -> Dict: 

462 return self._run_git("status", "--porcelain") 

463 

464 def _handle_git_log(self, max_count: int = 10, oneline: bool = True) -> Dict: 

465 args = ["log", f"-n{max_count}"] 

466 if oneline: args.append("--oneline") 

467 return self._run_git(*args) 

468 

469 def _handle_git_diff(self, staged: bool = False, commit: str = "") -> Dict: 

470 args = ["diff"] 

471 if staged: args.append("--staged") 

472 if commit: args.append(commit) 

473 return self._run_git(*args) 

474 

475 def _handle_git_branch(self, remote: bool = False) -> Dict: 

476 args = ["branch"] 

477 if remote: args.append("-r") 

478 return self._run_git(*args) 

479 

480 

481# ── Shell Server (3 tools) ─────────────────── 

482 

483 

484class ShellServer: 

485 """MCP-compatible safe shell command execution.""" 

486 

487 NAME = "shell" 

488 VERSION = "1.0.0" 

489 

490 SAFE_COMMANDS = {"ls", "cat", "head", "tail", "wc", "grep", "find", "du", "df", 

491 "echo", "date", "whoami", "uname", "pwd", "which", "env", "ps", 

492 "top", "htop", "tree", "file", "stat", "md5sum", "sha256sum", 

493 "python3", "python", "pip", "npm", "node", "curl", "wget"} 

494 

495 def _is_safe(self, cmd: str) -> bool: 

496 base = cmd.strip().split()[0] if cmd.strip() else "" 

497 return base in self.SAFE_COMMANDS 

498 

499 def get_tools(self) -> List[Dict[str, Any]]: 

500 return [ 

501 {"name": "run_command", "description": "Execute a safe shell command", 

502 "inputSchema": {"type": "object", "properties": { 

503 "command": {"type": "string"}, "timeout": {"type": "integer", "default": 30}}, 

504 "required": ["command"]}}, 

505 {"name": "system_info", "description": "Get system information (OS, CPU, memory)", 

506 "inputSchema": {"type": "object", "properties": {}}}, 

507 {"name": "disk_usage", "description": "Show disk usage for a path", 

508 "inputSchema": {"type": "object", "properties": { 

509 "path": {"type": "string", "default": "."}}}}, 

510 ] 

511 

512 def call_tool(self, tool_name: str, arguments: Dict) -> Any: 

513 return getattr(self, f"_handle_{tool_name}")(**arguments) 

514 

515 def _handle_run_command(self, command: str, timeout: int = 30) -> Dict: 

516 if not self._is_safe(command): 

517 return {"error": f"Command not in safelist. Allowed: {sorted(self.SAFE_COMMANDS)}"} 

518 try: 

519 r = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=timeout) 

520 return {"stdout": r.stdout, "stderr": r.stderr, "returncode": r.returncode} 

521 except subprocess.TimeoutExpired: return {"error": "Timeout"} 

522 except Exception as e: return {"error": str(e)} 

523 

524 def _handle_system_info(self) -> Dict: 

525 import platform 

526 return {"os": platform.system(), "release": platform.release(), "version": platform.version(), 

527 "machine": platform.machine(), "processor": platform.processor(), 

528 "python": platform.python_version(), "hostname": platform.node()} 

529 

530 def _handle_disk_usage(self, path: str = ".") -> Dict: 

531 try: 

532 usage = shutil.disk_usage(path) 

533 return {"path": path, "total_gb": round(usage.total/1024**3, 2), 

534 "used_gb": round(usage.used/1024**3, 2), "free_gb": round(usage.free/1024**3, 2)} 

535 except Exception as e: return {"error": str(e)} 

536 

537 

538# ── Code Server (3 tools) ──────────────────── 

539 

540 

541class CodeServer: 

542 """MCP-compatible sandboxed code execution.""" 

543 

544 NAME = "code" 

545 VERSION = "1.0.0" 

546 

547 def get_tools(self) -> List[Dict[str, Any]]: 

548 return [ 

549 {"name": "run_python", "description": "Execute Python code in sandbox", 

550 "inputSchema": {"type": "object", "properties": { 

551 "code": {"type": "string"}, "timeout": {"type": "integer", "default": 10}}, 

552 "required": ["code"]}}, 

553 {"name": "run_shell", "description": "Execute a one-liner bash command", 

554 "inputSchema": {"type": "object", "properties": { 

555 "command": {"type": "string"}, "timeout": {"type": "integer", "default": 10}}, 

556 "required": ["command"]}}, 

557 {"name": "lint_code", "description": "Basic code linting (syntax check)", 

558 "inputSchema": {"type": "object", "properties": { 

559 "code": {"type": "string"}, "language": {"type": "string", "default": "python"}}, 

560 "required": ["code"]}}, 

561 ] 

562 

563 def call_tool(self, tool_name: str, arguments: Dict) -> Any: 

564 return getattr(self, f"_handle_{tool_name}")(**arguments) 

565 

566 def _handle_run_python(self, code: str, timeout: int = 10) -> Dict: 

567 try: 

568 # Restricted execution via compile + eval in limited namespace 

569 restricted_globals = {"__builtins__": { 

570 "print": print, "len": len, "range": range, "int": int, "float": float, 

571 "str": str, "list": list, "dict": dict, "bool": bool, "set": set, "tuple": tuple, 

572 "sum": sum, "min": min, "max": max, "abs": abs, "round": round, "sorted": sorted, 

573 "enumerate": enumerate, "zip": zip, "map": map, "filter": filter, 

574 "json": __import__("json"), "math": __import__("math"), 

575 "datetime": __import__("datetime"), "re": __import__("re"), 

576 "collections": __import__("collections"), "itertools": __import__("itertools"), 

577 }} 

578 import io, sys 

579 old_stdout = sys.stdout 

580 sys.stdout = buffer = io.StringIO() 

581 try: 

582 compiled = compile(code, "<mcp_sandbox>", "exec") 

583 exec(compiled, restricted_globals) 

584 output = buffer.getvalue() 

585 finally: 

586 sys.stdout = old_stdout 

587 return {"output": output, "success": True} 

588 except Exception as e: 

589 return {"output": str(e), "success": False, "error": type(e).__name__} 

590 

591 def _handle_run_shell(self, command: str, timeout: int = 10) -> Dict: 

592 try: 

593 r = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=timeout) 

594 return {"stdout": r.stdout, "stderr": r.stderr, "returncode": r.returncode} 

595 except subprocess.TimeoutExpired: return {"error": "Timeout"} 

596 except Exception as e: return {"error": str(e)} 

597 

598 def _handle_lint_code(self, code: str, language: str = "python") -> Dict: 

599 if language == "python": 

600 try: 

601 compile(code, "<lint>", "exec") 

602 return {"valid": True, "errors": []} 

603 except SyntaxError as e: 

604 return {"valid": False, "errors": [{"line": e.lineno, "offset": e.offset, "message": e.msg}]} 

605 return {"valid": None, "message": f"Linting not supported for {language}"} 

606 

607 

608# ── Text Server (4 tools) ──────────────────── 

609 

610 

611class TextServer: 

612 """MCP-compatible text manipulation tools.""" 

613 

614 NAME = "text" 

615 VERSION = "1.0.0" 

616 

617 def get_tools(self) -> List[Dict[str, Any]]: 

618 return [ 

619 {"name": "count_tokens", "description": "Estimate token count (OpenAI tiktoken-style approximate)", 

620 "inputSchema": {"type": "object", "properties": { 

621 "text": {"type": "string"}}, "required": ["text"]}}, 

622 {"name": "extract_regex", "description": "Extract patterns from text using regex", 

623 "inputSchema": {"type": "object", "properties": { 

624 "text": {"type": "string"}, "pattern": {"type": "string"}, "group": {"type": "integer", "default": 0}}, 

625 "required": ["text", "pattern"]}}, 

626 {"name": "summarize_text", "description": "Simple extractive text summarization", 

627 "inputSchema": {"type": "object", "properties": { 

628 "text": {"type": "string"}, "max_sentences": {"type": "integer", "default": 5}}, 

629 "required": ["text"]}}, 

630 {"name": "format_json", "description": "Format/validate/prettify JSON", 

631 "inputSchema": {"type": "object", "properties": { 

632 "text": {"type": "string"}, "indent": {"type": "integer", "default": 2}}, 

633 "required": ["text"]}}, 

634 ] 

635 

636 def call_tool(self, tool_name: str, arguments: Dict) -> Any: 

637 return getattr(self, f"_handle_{tool_name}")(**arguments) 

638 

639 def _handle_count_tokens(self, text: str) -> Dict: 

640 # Approximate: ~4 chars per token for English, ~1.5 for CJK 

641 words = len(re.findall(r'\w+', text)) 

642 chars = len(text) 

643 return {"tokens_approx": max(1, words + chars // 4), "characters": chars, "words": words} 

644 

645 def _handle_extract_regex(self, text: str, pattern: str, group: int = 0) -> List[str]: 

646 try: 

647 return [m.group(group) if group else m.group(0) for m in re.finditer(pattern, text)] 

648 except re.error as e: return [f"Invalid regex: {e}"] 

649 

650 def _handle_summarize_text(self, text: str, max_sentences: int = 5) -> str: 

651 sentences = re.split(r'(?<=[.!?])\s+', text) 

652 if len(sentences) <= max_sentences: return text 

653 # Simple extractive: take first sentence + longest sentences 

654 first = sentences[0] 

655 rest = sorted(sentences[1:], key=len, reverse=True)[:max_sentences - 1] 

656 return ". ".join([first] + rest) + "." 

657 

658 def _handle_format_json(self, text: str, indent: int = 2) -> Dict: 

659 try: 

660 data = json.loads(text) 

661 formatted = json.dumps(data, indent=indent, ensure_ascii=False) 

662 return {"valid": True, "formatted": formatted, "keys": list(data.keys()) if isinstance(data, dict) else None} 

663 except json.JSONDecodeError as e: 

664 return {"valid": False, "error": str(e)} 

665 

666 

667# ── Built-in Server Registry ───────────────── 

668 

669 

670class BuiltinMCPRegistry: 

671 """Registry of all built-in MCP servers. Single interface for tool discovery/calling.""" 

672 

673 def __init__(self): 

674 self._servers: Dict[str, Any] = {} 

675 

676 def register_server(self, server: Any) -> None: 

677 self._servers[server.NAME] = server 

678 

679 def list_all_tools(self) -> List[Dict[str, Any]]: 

680 tools = [] 

681 for srv_name, server in self._servers.items(): 

682 for tool in server.get_tools(): 

683 tools.append({"server": srv_name, "name": f"mcp__{srv_name}__{tool['name']}", 

684 "description": tool.get("description", ""), "inputSchema": tool.get("inputSchema", {})}) 

685 return tools 

686 

687 def get_tool_schemas(self, format: str = "openai") -> List[Dict[str, Any]]: 

688 schemas = [] 

689 for srv_name, server in self._servers.items(): 

690 for tool in server.get_tools(): 

691 schemas.append({"type": "function", 

692 "function": {"name": f"mcp__{srv_name}__{tool['name']}", 

693 "description": tool.get("description", ""), 

694 "parameters": tool.get("inputSchema", {})}}) 

695 return schemas 

696 

697 def call_tool(self, server_name: str, tool_name: str, arguments: Dict[str, Any]) -> Any: 

698 server = self._servers.get(server_name) 

699 if not server: raise ValueError(f"Server '{server_name}' not found") 

700 return server.call_tool(tool_name, arguments) 

701 

702 def call_tool_by_full_name(self, full_name: str, arguments: Dict[str, Any]) -> Any: 

703 parts = full_name.split("__", 2) 

704 if len(parts) != 3 or parts[0] != "mcp": raise ValueError(f"Invalid tool name: {full_name}") 

705 return self.call_tool(parts[1], parts[2], arguments) 

706 

707 @property 

708 def server_names(self) -> List[str]: return list(self._servers.keys()) 

709 

710 @property 

711 def tool_count(self) -> int: return sum(len(s.get_tools()) for s in self._servers.values()) 

712 

713 

714def create_default_registry( 

715 allowed_paths: Optional[List[str]] = None, 

716 memory_path: Optional[str] = None, 

717 repo_path: str = "", 

718) -> BuiltinMCPRegistry: 

719 """Create a BuiltinMCPRegistry with all 8 servers registered.""" 

720 if allowed_paths is None: allowed_paths = [os.getcwd(), str(Path.home())] 

721 reg = BuiltinMCPRegistry() 

722 reg.register_server(FilesystemServer(allowed_paths=allowed_paths)) 

723 reg.register_server(WebFetchServer()) 

724 reg.register_server(MemoryServer(storage_path=memory_path or "")) 

725 reg.register_server(SearchServer()) 

726 reg.register_server(GitServer(repo_path=repo_path)) 

727 reg.register_server(ShellServer()) 

728 reg.register_server(CodeServer()) 

729 reg.register_server(TextServer()) 

730 return reg