Initial commit: MCP server for Presenton integration
- server.py: MCP wrapper para API REST slide.descomplicar.pt - requirements.txt: fastmcp, httpx - .gitignore: Python standard
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -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
|
||||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fastmcp>=2.0.0
|
||||||
|
httpx>=0.27.0
|
||||||
170
server.py
Normal file
170
server.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user