Dies ist eine alte Version des Dokuments!
#mcp.json
# mcpserver.py import os import re import subprocess from typing import Optional, List import fitz # PyMuPDF import requests from bs4 import BeautifulSoup from mcp.server.fastmcp import FastMCP mcp = FastMCP("my-local-tools", json_response=True) # --- Safety rails --- ALLOWED_ROOT = os.path.abspath(os.environ.get("MCP_ALLOWED_ROOT", os.getcwd())) ENABLE_DANGEROUS = os.environ.get("MCP_ENABLE_DANGEROUS", "0") == "1" def _abspath_safe(path: str) -> str: ap = os.path.abspath(path) # nur innerhalb allowed root erlauben if os.path.commonpath([ALLOWED_ROOT, ap]) != ALLOWED_ROOT: raise ValueError(f"Path not allowed. Allowed root: {ALLOWED_ROOT}") return ap @mcp.tool() def list_files(directory: str) -> List[str]: """Listet Dateien und Ordner in einem Verzeichnis (innerhalb MCP_ALLOWED_ROOT).""" d = _abspath_safe(directory) items = os.listdir(d) return sorted(items) @mcp.tool() def read_file(path: str, start_line: int = 1, max_lines: int = 400, tail_lines: Optional[int] = None) -> str: """Liest Textdatei teilweise (zeilenbasiert), um Kontext zu sparen.""" p = _abspath_safe(path) with open(p, "r", encoding="utf-8", errors="replace") as f: lines = f.readlines() if tail_lines and tail_lines > 0: sel = lines[-tail_lines:] start_idx = max(0, len(lines) - tail_lines) else: start_idx = max(0, start_line - 1) sel = lines[start_idx : start_idx + max(1, max_lines)] header = ( f"[read_file] path={p}\n" f"[read_file] total_lines={len(lines)} selected_lines={len(sel)} " f"range={start_idx+1}-{start_idx+len(sel)}\n\n" ) return header + "".join(sel) @mcp.tool() def read_pdf_text(path: str, start_page: int = 1, max_pages: int = 5, max_chars: int = 12000) -> str: """Extrahiert Text aus einem PDF (Text-Layer).""" p = _abspath_safe(path) doc = fitz.open(p) n = doc.page_count sp = max(1, start_page) ep = min(n, sp + max_pages - 1) chunks = [] for pno in range(sp - 1, ep): page = doc.load_page(pno) txt = page.get_text("text") chunks.append(f"\n--- Seite {pno+1}/{n} ---\n{txt}") out = "".join(chunks).strip() if len(out) > max_chars: out = out[:max_chars] + "\n…[gekürzt]" return f"[read_pdf_text] path={p} pages={sp}-{ep}/{n}\n\n{out}" @mcp.tool() def fetch_html(url: str, max_chars: int = 8000) -> str: """Gibt sichtbaren Text einer http(s)-Seite zurück.""" if not re.match(r"^https?://", url, flags=re.IGNORECASE): raise ValueError("Only http(s) URLs allowed") headers = {"User-Agent": "Mozilla/5.0 (compatible; MCP-Tools/1.0)"} resp = requests.get(url, headers=headers, timeout=25, allow_redirects=True) resp.raise_for_status() soup = BeautifulSoup(resp.text, "html.parser") for tag in soup(["script", "style", "noscript", "template"]): tag.decompose() text = soup.get_text(separator="\n", strip=True) text = re.sub(r"\n{3,}", "\n\n", text) if len(text) > max_chars: text = text[:max_chars] + "\n…[gekürzt]" return text @mcp.tool() def execute_cmd(command: str) -> str: """Führt einen Shell-Befehl aus (standardmäßig deaktiviert!).""" if not ENABLE_DANGEROUS: return "execute_cmd ist deaktiviert. Setze MCP_ENABLE_DANGEROUS=1 wenn du das wirklich willst." r = subprocess.run(command, shell=True, capture_output=True, text=True, encoding="utf-8", errors="replace") return r.stdout if r.returncode == 0 else f"Fehler: {r.stderr or r.stdout}" @mcp.tool() def execute_powershell(command: str) -> str: """Führt PowerShell aus (standardmäßig deaktiviert!).""" if not ENABLE_DANGEROUS: return "execute_powershell ist deaktiviert. Setze MCP_ENABLE_DANGEROUS=1 wenn du das wirklich willst." ps = ["powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", command] r = subprocess.run(ps, capture_output=True, text=True, encoding="utf-8", errors="replace") return r.stdout if r.returncode == 0 else f"Fehler: {r.stderr or r.stdout}" if __name__ == "__main__": # Für LM Studio als "command server" ist stdio typischerweise am einfachsten mcp.run(transport="stdio")