Add image support to Files and contact photos to Contacts

Files:
- read_file returns images inline (jpg/png/gif/webp up to 10MB)
- Text files returned as text, binary files as metadata only

Contacts:
- get_contact includes contact photo as inline image if available
- New tool: set_contact_photo (base64 jpeg/png → VCard PHOTO)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Stefan Lohmaier
2026-06-12 07:53:55 +02:00
parent 924366ac6c
commit a9359beead
2 changed files with 74 additions and 18 deletions
+20 -12
View File
@@ -1,12 +1,13 @@
"""MCP Files Server — browse and read files via WebDAV/oCIS."""
import os, sys, contextlib
import os, sys, contextlib, base64
from xml.etree import ElementTree as ET
from typing import Annotated
import httpx
from pydantic import Field
from mcp.server.fastmcp import FastMCP
from mcp.types import TextContent, ImageContent
from starlette.applications import Starlette
from starlette.routing import Mount
@@ -68,20 +69,27 @@ def list_files(
return "\n".join(lines) if lines else "Leeres Verzeichnis"
IMAGE_TYPES = {"image/jpeg", "image/png", "image/gif", "image/webp", "image/svg+xml"}
TEXT_HINTS = ["text/", "json", "xml", "csv", "yaml", "javascript", "markdown"]
@mcp.tool()
def read_file(
path: Annotated[str, Field(description="Full file path, e.g. '/Documents/notes.txt', '/config.yaml'")],
) -> str:
"""Read a text file's content (txt, md, json, csv, yaml, xml, html). Binary files show only metadata."""
path: Annotated[str, Field(description="Full file path, e.g. '/Documents/notes.txt', '/Photos/pic.jpg'")],
) -> list[TextContent | ImageContent]:
"""Read a file. Text files return content directly. Images (jpg/png/gif/webp) are displayed inline. Other binary files return only metadata."""
user = get_current_user()
if not user: return "Error: not authenticated"
r = httpx.get(_dav(user, path), auth=_auth(user), timeout=30)
if r.status_code >= 400: return f"Fehler: HTTP {r.status_code}"
ct = r.headers.get("content-type", "")
if any(t in ct for t in ["text/", "json", "xml", "csv", "yaml", "javascript"]):
return r.text[:100000]
try: return r.content.decode("utf-8")[:100000]
except: return f"Binaerdatei ({len(r.content)} bytes, Typ: {ct}). Kann nicht angezeigt werden."
if not user: return [TextContent(type="text", text="Error: not authenticated")]
r = httpx.get(_dav(user, path), auth=_auth(user), timeout=60)
if r.status_code >= 400: return [TextContent(type="text", text=f"Fehler: HTTP {r.status_code}")]
ct = r.headers.get("content-type", "").split(";")[0].strip()
if ct in IMAGE_TYPES and len(r.content) < 10_000_000:
return [ImageContent(type="image", data=base64.b64encode(r.content).decode(), mimeType=ct)]
if any(t in ct for t in TEXT_HINTS):
return [TextContent(type="text", text=r.text[:100000])]
try:
return [TextContent(type="text", text=r.content.decode("utf-8")[:100000])]
except Exception:
return [TextContent(type="text", text=f"Binaerdatei ({len(r.content):,} bytes, Typ: {ct}). Nutze read_file nur fuer Text und Bilder.")]
@mcp.tool()