Skip to main content
Glama
server.py6.63 kB
"""MCP server for generating memes with text overlays.""" import argparse import time from pathlib import Path from typing import Annotated from fastmcp import FastMCP from pydantic import Field from PIL import Image, ImageDraw, ImageFont from app.utils import get_fallback_font_path, get_meme_configs, get_output_dir, get_templates_dir, init, set_dev_mode mcp = FastMCP("meme-generator") def wrap_text( draw: ImageDraw.ImageDraw, text: str, font: ImageFont.FreeTypeFont | ImageFont.ImageFont, max_width: int, ) -> list[str]: """Wrap text to fit within max_width pixels.""" lines: list[str] = [] current_line: list[str] = [] for word in text.upper().split(): current_line.append(word) test_line = " ".join(current_line) bbox = draw.textbbox((0, 0), test_line, font=font) if bbox[2] - bbox[0] > max_width: if len(current_line) > 1: current_line.pop() lines.append(" ".join(current_line)) current_line = [word] else: lines.append(test_line) current_line = [] if current_line: lines.append(" ".join(current_line)) return lines def generate_meme_image( meme_type: str, texts: dict[str, str], output_dir: Path ) -> Path: """Generate a meme image with the given texts.""" config = get_meme_configs()[meme_type] template_path = get_templates_dir() / config.template_file if not template_path.exists(): raise FileNotFoundError(f"Template not found: {config.template_file}") img = Image.open(template_path) draw = ImageDraw.Draw(img) for name, text in texts.items(): placeholder = config.placeholders[name] try: font = ImageFont.truetype("Impact", placeholder.font_size) except OSError: try: font = ImageFont.truetype(str(get_fallback_font_path()), placeholder.font_size) except OSError: font = ImageFont.load_default(placeholder.font_size) lines = wrap_text(draw, text, font, placeholder.max_width) anchor_map = {"left": "la", "center": "ma", "right": "ra"} anchor = anchor_map[placeholder.align] y_offset = placeholder.y for line in lines: bbox = draw.textbbox((0, 0), line, font=font) line_height = bbox[3] - bbox[1] draw.text( (placeholder.x, y_offset), line, font=font, fill=placeholder.fill, stroke_width=placeholder.stroke_width, stroke_fill=placeholder.stroke_fill, anchor=anchor, ) y_offset += line_height + int(placeholder.font_size * 0.2) output_path = output_dir / f"{int(time.time())}_{meme_type}.jpg" img.save(output_path, "JPEG") return output_path @mcp.tool() async def generate_meme( meme_name: Annotated[str, Field(description="The type of meme to generate")], texts: Annotated[ dict[str, str], Field( description=( 'Dictionary like {"placeholder_name": "Your text here"}. ' "To skip a placeholder, use an empty string explicitly." ) ), ], ) -> dict: """ Generate a meme with custom text overlays. Each meme type has specific named text placeholders that must be filled. Use the 'get_meme_info' tool to see available memes and their placeholder requirements. """ try: meme_configs = get_meme_configs() if meme_name not in meme_configs: return { "status": "error", "message": f"Unknown meme type: {meme_name}", "available_memes": list(meme_configs.keys()), } config = meme_configs[meme_name] expected_keys = set(config.placeholders.keys()) provided_keys = set(texts.keys()) if provided_keys != expected_keys: missing = expected_keys - provided_keys extra = provided_keys - expected_keys error_parts = [] if missing: error_parts.append(f"missing: {', '.join(missing)}") if extra: error_parts.append(f"unexpected: {', '.join(extra)}") msg = f"Meme '{meme_name}' placeholder mismatch. {'; '.join(error_parts)}." if missing: msg += " To skip a placeholder, use an empty string explicitly." raise ValueError(msg) saved_path = generate_meme_image(meme_name, texts, get_output_dir()) return { "status": "success", "message": "Meme generated successfully", "output_path": str(saved_path.resolve()), "meme_type": meme_name, "texts_used": texts, } except Exception as e: return { "status": "error", "message": f"Error generating meme: {str(e)}", "meme_type": meme_name, } @mcp.tool() async def get_meme_info( meme_name: Annotated[ str | None, Field(description="Optional: Get info for a specific meme") ] = None, ) -> dict: """Get information about available memes and their text placeholder requirements.""" meme_configs = get_meme_configs() if meme_name: if meme_name not in meme_configs: return {"status": "error", "message": f"Unknown meme type: {meme_name}"} config = meme_configs[meme_name] placeholder_names = list(config.placeholders.keys()) return { "status": "success", "meme_name": meme_name, "template_file": config.template_file, "placeholder_names": placeholder_names, "example_usage": { "meme_name": meme_name, "texts": {name: f"Example {name}" for name in placeholder_names}, }, } else: all_memes = {} for meme_type, config in meme_configs.items(): all_memes[meme_type] = { "placeholder_names": list(config.placeholders.keys()), "template_file": config.template_file, } return { "status": "success", "available_memes": all_memes, "total_memes": len(all_memes), } def main(): parser = argparse.ArgumentParser() parser.add_argument("--dev", action="store_true", help="Use local app/ templates and configs") args = parser.parse_args() if args.dev: set_dev_mode(True) init() mcp.run()

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/t0ster/meme-generator-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server