From f7032262ee7f0e6b1a72164b8c1ad8866f40e807 Mon Sep 17 00:00:00 2001 From: Emanuel Almeida Date: Wed, 4 Mar 2026 19:58:46 +0000 Subject: [PATCH] Initial commit: MCP server for Presenton integration - server.py: MCP wrapper para API REST slide.descomplicar.pt - requirements.txt: fastmcp, httpx - .gitignore: Python standard --- .gitignore | 24 +++++++ requirements.txt | 2 + server.py | 170 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 196 insertions(+) create mode 100644 .gitignore create mode 100644 requirements.txt create mode 100644 server.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8bf5a6c --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +.venv/ +ENV/ +dist/ +build/ +*.egg-info/ +.eggs/ +pip-log.txt +pip-delete-this-directory.txt +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml +*.log +.env +.env.* +*.sqlite3 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d807ec1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +fastmcp>=2.0.0 +httpx>=0.27.0 diff --git a/server.py b/server.py new file mode 100644 index 0000000..4e0a7a1 --- /dev/null +++ b/server.py @@ -0,0 +1,170 @@ +""" +MCP Presenton - Wrapper API REST para slide.descomplicar.pt +Gera apresentacoes AI via Presenton self-hosted. +""" + +import os +import json +from typing import Optional + +import httpx +from fastmcp import FastMCP + +mcp = FastMCP("presenton") + +BASE_URL = os.environ.get("PRESENTON_URL", "https://slide.descomplicar.pt") +API_KEY = os.environ.get("PRESENTON_API_KEY", "") + + +def _headers() -> dict: + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + if API_KEY: + headers["Authorization"] = f"Bearer {API_KEY}" + return headers + + +def _api_url(path: str) -> str: + return f"{BASE_URL}/api/v1{path}" + + +async def _post(path: str, data: dict) -> dict: + async with httpx.AsyncClient(timeout=120.0) as client: + resp = await client.post(_api_url(path), json=data, headers=_headers()) + resp.raise_for_status() + return resp.json() + + +async def _get(path: str, params: dict | None = None) -> dict: + async with httpx.AsyncClient(timeout=60.0) as client: + resp = await client.get(_api_url(path), params=params, headers=_headers()) + resp.raise_for_status() + return resp.json() + + +@mcp.tool +async def generate_presentation( + content: str, + n_slides: int = 10, + tone: str = "professional", + template: Optional[str] = None, + language: str = "pt", + web_search: bool = False, +) -> dict: + """Gera apresentacao AI a partir de prompt de texto. + + Args: + content: Tema ou descricao da apresentacao. + n_slides: Numero de slides (default 10). + tone: Tom da apresentacao (professional, casual, sales_pitch, educational). + template: ID do template a usar (opcional). + language: Idioma (default pt). + web_search: Pesquisar web para enriquecer conteudo. + + Returns: + Dict com presentation_id, status e detalhes. + """ + payload = { + "content": content, + "n_slides": n_slides, + "tone": tone, + "language": language, + "web_search": web_search, + } + if template: + payload["template"] = template + + return await _post("/ppt/presentation/generate", payload) + + +@mcp.tool +async def generate_from_json( + slides: list[dict], + template: Optional[str] = None, +) -> dict: + """Gera apresentacao a partir de estrutura JSON deterministica. + + Args: + slides: Lista de slides, cada um com title, content, layout, notes. + template: ID do template (opcional). + + Returns: + Dict com presentation_id e path. + """ + payload = {"slides": slides} + if template: + payload["template"] = template + + return await _post("/ppt/presentation/create", payload) + + +@mcp.tool +async def export_presentation( + presentation_id: str, + format: str = "pptx", +) -> dict: + """Exporta apresentacao como PPTX ou PDF. + + Args: + presentation_id: ID da apresentacao a exportar. + format: Formato de exportacao (pptx ou pdf). + + Returns: + Dict com download_url e path do ficheiro. + """ + return await _post("/ppt/presentation/export", { + "id": presentation_id, + "export_as": format, + }) + + +@mcp.tool +async def list_templates() -> list[dict]: + """Lista templates disponiveis (standard + AI-generated). + + Returns: + Lista de templates com id, name, thumbnail, category. + """ + result = await _get("/ppt/template-management/summary") + return result if isinstance(result, list) else result.get("presentations", []) + + +@mcp.tool +async def list_presentations() -> list[dict]: + """Lista apresentacoes existentes. + + Returns: + Lista de apresentacoes com id, title, created_at, slide_count. + """ + result = await _get("/ppt/presentation/all") + return result if isinstance(result, list) else result.get("presentations", []) + + +@mcp.tool +async def upload_image(image_path: str) -> dict: + """Upload imagem para biblioteca Presenton para uso em slides. + + Args: + image_path: Caminho local do ficheiro de imagem. + + Returns: + Dict com image_id e url da imagem no Presenton. + """ + async with httpx.AsyncClient(timeout=60.0) as client: + with open(image_path, "rb") as f: + img_headers = {} + if API_KEY: + img_headers["Authorization"] = f"Bearer {API_KEY}" + resp = await client.post( + _api_url("/ppt/images/upload"), + headers=img_headers, + files={"file": (os.path.basename(image_path), f)}, + ) + resp.raise_for_status() + return resp.json() + + +if __name__ == "__main__": + mcp.run()