"""Office-Dokumente (docx/xlsx/pptx) -> extrahierter Text fuer Files- und Mail-MCP. Analog zu pdfutil: server-seitige Extraktion, damit das LLM (OpenClaw wie claude.ai) den Inhalt als Text bekommt statt eines Base64-Blobs. Aeltere Binaerformate (.doc/.xls/.ppt) und .rtf koennen diese Libs nicht -> hier NICHT behandelt (is_office() liefert False, Aufrufer faellt auf Rohdaten zurueck). """ import base64 import io from mcp.types import TextContent, EmbeddedResource, BlobResourceContents MAX_TEXT = 200000 DOCX = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" PPTX = "application/vnd.openxmlformats-officedocument.presentationml.presentation" _EXT = {".docx": DOCX, ".xlsx": XLSX, ".pptx": PPTX} def _docx_text(content): import docx d = docx.Document(io.BytesIO(content)) parts = [] for p in d.paragraphs: if p.text.strip(): parts.append(p.text) for ti, tbl in enumerate(d.tables, 1): parts.append(f"[Tabelle {ti}]") for row in tbl.rows: parts.append(" | ".join(c.text.strip() for c in row.cells)) return "\n".join(parts) def _xlsx_text(content): import openpyxl wb = openpyxl.load_workbook(io.BytesIO(content), read_only=True, data_only=True) parts = [] try: for ws in wb.worksheets: parts.append(f"--- Blatt: {ws.title} ---") for row in ws.iter_rows(values_only=True): if any(v is not None for v in row): parts.append("\t".join("" if v is None else str(v) for v in row)) finally: wb.close() return "\n".join(parts) def _pptx_text(content): from pptx import Presentation prs = Presentation(io.BytesIO(content)) parts = [] for i, slide in enumerate(prs.slides, 1): parts.append(f"--- Folie {i} ---") for shape in slide.shapes: if shape.has_text_frame: for para in shape.text_frame.paragraphs: t = "".join(run.text for run in para.runs) if t.strip(): parts.append(t) if getattr(shape, "has_table", False): for row in shape.table.rows: parts.append(" | ".join(c.text for c in row.cells)) return "\n".join(parts) _HANDLERS = { DOCX: ("Word-Dokument", _docx_text), XLSX: ("Excel-Tabelle", _xlsx_text), PPTX: ("PowerPoint", _pptx_text), } def _resolve(mime, label): if mime in _HANDLERS: return _HANDLERS[mime] ext = "." + label.rsplit(".", 1)[-1].lower() if "." in label else "" m = _EXT.get(ext) return _HANDLERS.get(m) if m else None def is_office(mime, label=""): """True fuer docx/xlsx/pptx (per MIME oder Endung).""" return _resolve(mime, label) is not None def office_to_content(content, label, mime="", uri=None): uri = uri or f"file://{label}" kind, fn = _resolve(mime, label) try: text = fn(content) except Exception as e: return [ TextContent(type="text", text=f"[{kind} '{label}' konnte nicht gelesen werden ({e}). Rohdaten folgen.]"), EmbeddedResource(type="resource", resource=BlobResourceContents( uri=uri, blob=base64.b64encode(content).decode(), mimeType=mime or "application/octet-stream")), ] if text.strip(): return [TextContent(type="text", text=f"[{kind}: {label}]\n\n{text[:MAX_TEXT]}")] return [TextContent(type="text", text=f"[{kind} '{label}' enthaelt keinen extrahierbaren Text.]")]