UNPKG

memecp

Version:

A simple Model Context Protocol (MCP) server

232 lines (231 loc) 9.41 kB
import { getMemeTemplates } from "../getMemeTemplates.js"; export const composeMemeToolDefinition = { name: "compose_meme", description: "Generate a meme by selecting an appropriate template and adding text using Memegen.link API", inputSchema: { type: "object", properties: { text: { type: "string", description: "The text content for the meme. Will be split appropriately based on template, or you can once specify a `|` to split the text into top and bottom text.", }, template: { type: "string", description: "Optional: Specific template name to use. If not provided, an appropriate template will be selected automatically", }, returnImage: { type: "boolean", description: "Whether to return the image in the response", default: true, }, }, required: ["text"], }, }; // Optional API key for authenticated requests (removes watermark) const MEMEGEN_API_KEY = process.env.MEMEGEN_API_KEY; function encodeText(text) { // Handle special characters for memegen.link URL encoding return text .replace(/_/g, "__") // underscore → double underscore .replace(/-/g, "--") // dash → double dash .replace(/ /g, "_") // space → underscore .replace(/\?/g, "~q") // question mark → ~q .replace(/&/g, "~a") // ampersand → ~a .replace(/%/g, "~p") // percentage → ~p .replace(/#/g, "~h") // hashtag → ~h .replace(/\//g, "~s") // slash → ~s .replace(/\\/g, "~b") // backslash → ~b .replace(/</g, "~l") // less-than → ~l .replace(/>/g, "~g") // greater-than → ~g .replace(/"/g, "''") // double quote → two single quotes .replace(/\n/g, "~n"); // newline → ~n } async function selectTemplate(text, userTemplate) { const templates = await getMemeTemplates(1, 10000000000); // If user specified a template, try to use it if (userTemplate) { const normalizedTemplate = userTemplate.trim(); // Find exact name match first const exactMatch = templates.find((t) => t.name.toLowerCase() === normalizedTemplate.toLowerCase()); if (exactMatch) { return exactMatch.id; } // Find exact ID match const idMatch = templates.find((t) => t.id.toLowerCase() === normalizedTemplate.toLowerCase()); if (idMatch) { return idMatch.id; } // Find partial name match const partialMatch = templates.find((t) => t.name.toLowerCase().includes(normalizedTemplate.toLowerCase()) || normalizedTemplate.toLowerCase().includes(t.name.toLowerCase())); if (partialMatch) { return partialMatch.id; } // Find match in keywords const keywordMatch = templates.find((t) => t.keywords && t.keywords.some((keyword) => keyword.toLowerCase().includes(normalizedTemplate.toLowerCase()))); if (keywordMatch) { return keywordMatch.id; } } // Auto-select template based on text content and common patterns const lowerText = text.toLowerCase(); // Find "Two Buttons" template for choices if (lowerText.includes("vs") || lowerText.includes("or") || lowerText.includes("choose")) { const twoButtons = templates.find((t) => t.name.toLowerCase().includes("two buttons") || t.id.includes("button") || (t.keywords && t.keywords.some((k) => k.includes("choice")))); if (twoButtons) return twoButtons.id; } // Find "Expanding Brain" template for intelligence if (lowerText.includes("brain") || lowerText.includes("smart") || lowerText.includes("intelligence")) { const expandingBrain = templates.find((t) => t.name.toLowerCase().includes("brain") || t.id.includes("brain") || (t.keywords && t.keywords.some((k) => k.includes("brain")))); if (expandingBrain) return expandingBrain.id; } // Find "Drake" template for preferences if ((lowerText.includes("like") && lowerText.includes("dislike")) || lowerText.includes("prefer")) { const drake = templates.find((t) => t.name.toLowerCase().includes("drake") || t.id.includes("drake")); if (drake) return drake.id; } // Find "This is Fine" template if (lowerText.includes("fine") || lowerText.includes("ok") || lowerText.includes("disaster")) { const thisIsFine = templates.find((t) => t.name.toLowerCase().includes("fine") || t.id.includes("fine")); if (thisIsFine) return thisIsFine.id; } // Find "Change My Mind" template if (lowerText.includes("change") && lowerText.includes("mind")) { const changeMind = templates.find((t) => (t.name.toLowerCase().includes("change") && t.name.toLowerCase().includes("mind")) || t.id.includes("cmm") || t.id.includes("change")); if (changeMind) return changeMind.id; } // Find "Distracted Boyfriend" template for comparisons if (lowerText.includes("distract") || (lowerText.includes("look") && lowerText.includes("at"))) { const distractedBf = templates.find((t) => t.name.toLowerCase().includes("distracted") || t.id.includes("boyfriend") || t.id.includes("distracted")); if (distractedBf) return distractedBf.id; } // Default to a common meme template (Fry is usually available) const fry = templates.find((t) => t.name.toLowerCase().includes("fry") || t.id.includes("fry")); if (fry) return fry.id; // Ultimate fallback - first template return templates[0].id; } function splitText(text, template) { // If text contains common separators, split on them const separators = [" vs ", " or ", "\n", " | ", " / "]; for (const separator of separators) { if (text.includes(separator)) { const parts = text.split(separator); return { topText: parts[0].trim(), bottomText: parts.slice(1).join(separator).trim(), }; } } // If no separator found, try to split intelligently based on template const words = text.split(" "); if (template.includes("brain")) { // For expanding brain, use all text in bottom return { topText: "", bottomText: text, }; } // Default split in half const midPoint = Math.ceil(words.length / 2); return { topText: words.slice(0, midPoint).join(" "), bottomText: words.slice(midPoint).join(" "), }; } export async function composeMeme(args) { const { text, template: userTemplate, returnImage } = args; try { // Select appropriate template const templateId = await selectTemplate(text, userTemplate); // Determine text placement let topText; let bottomText; // Auto-split the text const split = splitText(text, templateId); topText = split.topText; bottomText = split.bottomText; // Encode text for URL const encodedTopText = encodeText(topText); const encodedBottomText = encodeText(bottomText); // Build meme URL let memeUrl = `https://api.memegen.link/images/${templateId}`; if (encodedTopText || encodedBottomText) { memeUrl += `/${encodedTopText || "_"}/${encodedBottomText || "_"}.png`; } else { memeUrl += ".png"; } // Add API key if available if (MEMEGEN_API_KEY) { memeUrl += `?api_key=${MEMEGEN_API_KEY}`; } // Fetch the generated meme image const headers = {}; if (MEMEGEN_API_KEY) { headers["X-API-KEY"] = MEMEGEN_API_KEY; } const imageResponse = await fetch(memeUrl, { headers }); if (!imageResponse.ok) { throw new Error(`Failed to fetch generated meme: ${imageResponse.status} ${imageResponse.statusText}`); } if (returnImage) { const imageBuffer = await imageResponse.arrayBuffer(); const base64Data = Buffer.from(imageBuffer).toString("base64"); return { content: [ { type: "image", data: base64Data, mimeType: "image/png", }, { type: "text", text: memeUrl, }, ], }; } // // Get template name for display // const templates = await getMemeTemplates(); // const template = templates.find((t) => t.id === templateId); // const templateName = template?.name || `Template ID: ${templateId}`; return { content: [ { type: "text", text: memeUrl, }, ], }; } catch (error) { throw new Error(`Meme generation failed: ${error instanceof Error ? error.message : "Unknown error"}`); } }