Skip to main content
Glama

Glif

Official
by glifxyz
content-blocks.ts8.51 kB
import type { ContentBlock } from "@modelcontextprotocol/sdk/types.js"; import { getMimeType, isAudioUrl, isImageUrl, isVideoUrl, urlToBase64, } from "./utils.js"; /** * Type for glif output metadata with better type safety */ export interface GlifOutputMetadata { type: "IMAGE" | "AUDIO" | "VIDEO" | "JSON" | "HTML" | "TEXT" | string; [key: string]: unknown; } /** * Truncate base64 data in content blocks for cleaner logging */ export function truncateBase64InContentBlocks( blocks: ContentBlock[] ): ContentBlock[] { return blocks.map((block) => { if ((block.type === "image" || block.type === "audio") && "data" in block) { return { ...block, data: block.data ? "[base64_encoded_data_hidden]" : block.data, }; } return block; }); } /** * Create content blocks for multimedia URLs with multiple format support * Returns up to 3 content blocks: resource_link, base64 (if applicable), and text fallback */ async function createMultimediaBlocks( output: string, mediaType: "image" | "audio" | "video", emoji?: string ): Promise<ContentBlock[]> { const blocks: ContentBlock[] = []; const mimeType = getMimeType(output); const capitalizedType = mediaType.charAt(0).toUpperCase() + mediaType.slice(1); console.error(`[DEBUG] Adding multiple formats for ${mediaType}...`); // 1. Resource link for MCP-compliant clients blocks.push({ type: "resource_link", uri: output, name: `Generated ${capitalizedType}`, mimeType, }); // 2. Base64 embedded content for immediate display/playback (images and audio only) if (mediaType === "image" || mediaType === "audio") { try { console.error(`[DEBUG] Converting ${mediaType} to base64...`); const base64Data = await urlToBase64(output); if (mediaType === "image") { blocks.push({ type: "image", data: base64Data, mimeType, }); } else if (mediaType === "audio") { blocks.push({ type: "audio", data: base64Data, mimeType, }); } console.error(`[DEBUG] Successfully added base64 ${mediaType}`); } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); console.error(`[DEBUG] Failed to convert ${mediaType} to base64:`, { error: errorMsg, url: output, mediaType, }); // Log specific error types for monitoring and user feedback if (errorMsg.includes("timeout")) { console.error( `[WARNING] Network timeout for ${mediaType} URL: ${output}` ); } else if (errorMsg.includes("too large")) { console.error( `[WARNING] File size exceeded (max 10MB) for ${mediaType} URL: ${output}` ); } else if (errorMsg.includes("Private/local IP")) { console.error( `[SECURITY] SSRF attempt blocked for ${mediaType} URL: ${output}` ); } else if (errorMsg.includes("HTTP")) { console.error( `[WARNING] HTTP error fetching ${mediaType} from: ${output}` ); } else { console.error( `[WARNING] Unknown error fetching ${mediaType} from: ${output} - ${errorMsg}` ); } } } // 3. Text format for maximum compatibility const textContent = mediaType === "image" ? `![Generated ${capitalizedType}](${output})` : `${emoji || "📄"} ${capitalizedType}: ${output}`; blocks.push({ type: "text", text: textContent, }); return blocks; } /** * Create MCP-compliant content blocks from glif output */ export async function createContentBlocks( output: string | null, outputFull?: GlifOutputMetadata ): Promise<ContentBlock[]> { console.error("[DEBUG] createContentBlocks called with:", { output: `${output?.slice(0, 100)}...`, outputFull, }); if (!output || !outputFull?.type) { console.error("[DEBUG] Early return: missing output or outputFull.type"); return [{ type: "text", text: "No output received" }]; } const blocks: ContentBlock[] = []; const type = outputFull.type.toUpperCase(); console.error("[DEBUG] Processing type:", type); try { switch (type) { case "IMAGE": console.error( "[DEBUG] Processing IMAGE type, checking if isImageUrl:", { output, isImage: isImageUrl(output), } ); if (isImageUrl(output)) { blocks.push(...(await createMultimediaBlocks(output, "image"))); } else { console.error("[DEBUG] Not a valid image URL, using fallback"); blocks.push({ type: "text", text: `[Image] ${output}` }); } break; case "AUDIO": if (isAudioUrl(output)) { blocks.push(...(await createMultimediaBlocks(output, "audio", "🔊"))); } else { blocks.push({ type: "text", text: `[Audio] ${output}` }); } break; case "VIDEO": if (isVideoUrl(output)) { blocks.push(...(await createMultimediaBlocks(output, "video", "🎥"))); } else { blocks.push({ type: "text", text: `[Video] ${output}` }); } break; case "JSON": // For JSON, provide both structured and text formats for maximum compatibility // Add resource_link if this appears to be an API endpoint or data resource if (output.startsWith("http")) { blocks.push({ type: "resource_link", uri: output, name: "JSON Data", mimeType: "application/json", }); } // Always include text format with raw JSON for LLM consumption blocks.push({ type: "text", text: output }); break; case "HTML": // For HTML, provide rich content blocks for better client support // Add resource_link if this appears to be a web page URL if (output.startsWith("http")) { blocks.push({ type: "resource_link", uri: output, name: "Web Page", mimeType: "text/html", }); } else { // For HTML content, add resource_link with data URI for rich clients const dataUri = `data:text/html;charset=utf-8,${encodeURIComponent( output )}`; blocks.push({ type: "resource_link", uri: dataUri, name: "Generated HTML", mimeType: "text/html", }); } // Always include text format with raw HTML for LLM consumption blocks.push({ type: "text", text: output }); break; default: blocks.push({ type: "text", text: output }); break; } } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); console.error(`Error creating content blocks for ${type}:`, { error: errorMsg, output: output?.slice(0, 100), type, }); // Provide user-friendly fallback without exposing internal errors blocks.push({ type: "text", text: `[${type}] ${output}\n\n⚠️ Could not process multimedia content. The URL may be inaccessible or the file format unsupported.`, }); } return blocks.length > 0 ? blocks : [{ type: "text", text: output }]; } /** * Create structured content for JSON outputs using MCP's structuredContent field * This allows the LLM to receive structured data without formatting pollution */ export function createStructuredContent( output: string | null, outputFull?: GlifOutputMetadata ): Record<string, unknown> | null { // Only create structured content for JSON outputs if (outputFull?.type?.toUpperCase() === "JSON" && output) { try { const parsed = JSON.parse(output); if (typeof parsed === "object" && parsed !== null) { // For arrays, wrap in an object to maintain MCP compatibility if (Array.isArray(parsed)) { return { data: parsed }; } // Ensure we only return objects, not other types if (typeof parsed === "object" && !Array.isArray(parsed)) { return parsed; } return null; } } catch (parseError) { console.error("[DEBUG] Failed to parse JSON for structured content:", { error: parseError instanceof Error ? parseError.message : String(parseError), output: output.slice(0, 200), }); } } return null; }

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/glifxyz/glif-mcp-server'

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