Skip to main content
Glama
server.py37 kB
from typing import Any, Dict, List, Optional, Union import requests import uuid import json import base64 import re from PIL import Image import io from datetime import datetime import pytz from fastmcp import FastMCP from elevenlabs.client import ElevenLabs from elevenlabs import VoiceSettings from azure.storage.blob import BlobServiceClient from openai import AzureOpenAI import logging # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Configuration from environment variables import os from dotenv import load_dotenv load_dotenv() # Initialize FastMCP server mcp = FastMCP("super-singularity-api-server") API_BASE_URL = os.getenv("API_BASE_URL", "https://your-api-domain.com").strip('"').strip("'") API_TOKEN = os.getenv("API_TOKEN", "your-bearer-token-here").strip('"').strip("'") COMPANY_ID = os.getenv("COMPANY_ID", "your-company-id-here").strip('"').strip("'") # ElevenLabs configuration ELEVENLABS_API_KEY = os.getenv("ELEVENLABS_API_KEY", "your-elevenlabs-api-key").strip('"').strip("'") ELEVENLABS_VOICE_ID = os.getenv("ELEVENLABS_VOICE_ID", "21m00Tcm4TlvDq8ikWAM").strip('"').strip("'") # Default: Rachel # Azure Storage configuration AZURE_STORAGE_CONNECTION_STRING = os.getenv("AZURE_STORAGE_CONNECTION_STRING", "your-azure-connection-string").strip('"').strip("'") AZURE_CONTAINER_NAME = os.getenv("AZURE_CONTAINER_NAME", "audio-files").strip('"').strip("'") # Azure OpenAI configuration AZURE_OPENAI_API_KEY = os.getenv("AZURE_OPENAI_API_KEY", "your-azure-openai-api-key").strip('"').strip("'") AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT", "https://your-resource.cognitiveservices.azure.com").strip('"').strip("'") AZURE_OPENAI_DEPLOYMENT = os.getenv("AZURE_OPENAI_DEPLOYMENT", "gpt-image-1").strip('"').strip("'") AZURE_OPENAI_API_VERSION = os.getenv("AZURE_OPENAI_API_VERSION", "2025-04-01-preview").strip('"').strip("'") # Helper function for boolean parsing def parse_bool(val: Union[bool, str, None]) -> Optional[bool]: """Parse a boolean value from string or bool.""" if val is None: return None if isinstance(val, bool): return val if isinstance(val, str): return val.lower() == "true" return False def make_api_request(method: str, endpoint: str, data: Optional[Dict] = None) -> Dict[str, Any]: """Make authenticated API request to Super Singularity API.""" headers = { "Authorization": f"Bearer {API_TOKEN}", "Content-Type": "application/json" } url = f"{API_BASE_URL}{endpoint}" try: if method.upper() == "GET": response = requests.get(url, headers=headers, timeout=30.0) elif method.upper() == "POST": response = requests.post(url, headers=headers, json=data, timeout=30.0) elif method.upper() == "PUT": response = requests.put(url, headers=headers, json=data, timeout=30.0) else: return {"error": f"Unsupported HTTP method: {method}"} response.raise_for_status() return response.json() except requests.HTTPError as e: return {"error": f"HTTP {e.response.status_code}: {e.response.text}"} except Exception as e: return {"error": f"Request failed: {str(e)}"} def generate_uuid() -> str: """Generate a new UUID for courses and cards.""" return str(uuid.uuid4()) def generate_audio_with_elevenlabs(text: str) -> bytes: """Generate audio from text using ElevenLabs API. Args: text: The text to convert to speech Returns: Audio data as bytes """ try: # Initialize ElevenLabs client client = ElevenLabs(api_key=ELEVENLABS_API_KEY) # Generate audio using the sync API method audio = client.text_to_speech.convert( text=text, voice_id=ELEVENLABS_VOICE_ID, model_id="eleven_v3", voice_settings=VoiceSettings( stability=0.5, similarity_boost=0.5, style=0.0, use_speaker_boost=True ) ) # Convert generator to bytes audio_bytes = b"".join(audio) return audio_bytes except Exception as e: raise Exception(f"ElevenLabs audio generation failed: {str(e)}") def upload_to_azure(file_data: bytes, filename: str, file_type: str = "audio", file_extension: str = "mp3") -> str: """Upload file data to Azure Storage and return the public URL. Args: file_data: File data as bytes filename: Name for the uploaded file (without extension) file_type: Type of file (audio, video, image, etc.) for folder organization file_extension: File extension (mp3, mp4, jpg, png, etc.) Returns: Public URL of the uploaded file """ try: # Create blob service client blob_service_client = BlobServiceClient.from_connection_string(AZURE_STORAGE_CONNECTION_STRING) # Generate unique filename with folder structure blob_name = f"{file_type}/{filename}_{generate_uuid()[:8]}.{file_extension}" # Upload the file blob_client = blob_service_client.get_blob_client( container=AZURE_CONTAINER_NAME, blob=blob_name ) blob_client.upload_blob(file_data, overwrite=True) # Return the public URL return blob_client.url except Exception as e: raise Exception(f"Azure Storage upload failed: {str(e)}") def generate_and_upload_audio(text: str, title: str) -> str: """Generate audio from text and upload to Azure Storage. Args: text: The text to convert to speech title: Title for the audio file Returns: Public URL of the uploaded audio file """ # Generate audio audio_data = generate_audio_with_elevenlabs(text) # Upload to Azure filename = title.replace(" ", "_").lower() audio_url = upload_to_azure(audio_data, filename, "audio", "mp3") return audio_url def generate_image_with_azure_openai(prompt: str, size: str = "1024x1024", output_format: str = "png") -> bytes: """Generate image from prompt using Azure OpenAI API. Args: prompt: The prompt to generate image from size: Image size in format "WIDTHxHEIGHT" (e.g., "1024x1024") output_format: Output format ("png" or "jpg") Returns: Image data as bytes """ try: # Validate size format if not re.match(r'^\d+x\d+$', size): raise ValueError(f"Invalid size format: {size}. Must be in format 'WIDTHxHEIGHT'") # Validate output format if output_format not in ["png", "jpg"]: raise ValueError(f"Invalid output format: {output_format}. Must be 'png' or 'jpg'") logger.info(f"Attempting to generate image with Azure OpenAI Endpoint: {AZURE_OPENAI_ENDPOINT}") logger.info(f"Deployment: {AZURE_OPENAI_DEPLOYMENT}, API Version: {AZURE_OPENAI_API_VERSION}") url = f"{AZURE_OPENAI_ENDPOINT}/openai/deployments/{AZURE_OPENAI_DEPLOYMENT}/images/generations" headers = { "Content-Type": "application/json", "api-key": AZURE_OPENAI_API_KEY } data = { "prompt": prompt, "size": size, "quality": "medium", "output_compression": 100, "output_format": output_format, "n": 1 } response = requests.post( url, headers=headers, json=data, params={"api-version": AZURE_OPENAI_API_VERSION}, timeout=60.0 ) response.raise_for_status() result = response.json() # Extract base64 image data if "data" in result and len(result["data"]) > 0: b64_json = result["data"][0]["b64_json"] image_bytes = base64.b64decode(b64_json) logger.info("Image generated successfully") return image_bytes else: raise Exception("No image data returned from Azure OpenAI") except Exception as e: logger.error(f"Azure OpenAI image generation failed: {str(e)}") raise Exception(f"Azure OpenAI image generation failed: {str(e)}") def convert_image_to_webp(image_bytes: bytes, quality: int = 85) -> bytes: """Convert image bytes to WebP format with compression. Args: image_bytes: Original image bytes quality: WebP quality (0-100, default 85 for good quality/size balance) Returns: WebP image bytes """ try: # Open the image image = Image.open(io.BytesIO(image_bytes)) # Convert to RGB if necessary (WebP doesn't support all modes) if image.mode in ("RGBA", "P"): image = image.convert("RGB") # Save as WebP with compression output_buffer = io.BytesIO() image.save(output_buffer, format="WebP", quality=quality, optimize=True) return output_buffer.getvalue() except Exception as e: raise Exception(f"Image conversion to WebP failed: {str(e)}") def generate_and_upload_image(prompt: str, title: str, size: str = "1024x1024", output_format: str = "png") -> str: """Generate image from prompt and upload to Azure Storage. Args: prompt: The prompt to generate image from title: Title for the image file size: Image size in format "WIDTHxHEIGHT" output_format: Original format from OpenAI ("png" or "jpg") Returns: Public URL of the uploaded image file """ # Generate image image_data = generate_image_with_azure_openai(prompt, size, output_format) # Convert to WebP for better compression and web optimization webp_data = convert_image_to_webp(image_data, quality=85) # Upload to Azure filename = title.replace(" ", "_").lower() image_url = upload_to_azure(webp_data, filename, "images", "webp") return image_url @mcp.tool() def get_course(course_id: str) -> str: """Get details of a specific course. Args: course_id: The ID of the course to retrieve """ result = make_api_request("GET", f"/api/course?id={course_id}") return json.dumps(result, indent=2) @mcp.tool() def create_course( title: str, duration: Optional[Union[int, str]] = 0, description: Optional[str] = None, folder_id: Optional[str] = None, finalized_course_plan: Optional[str] = None, is_published: Union[bool, str] = False, is_autoplay: Union[bool, str] = False, is_scorable: Union[bool, str] = False, gradient_from_color: Optional[str] = None, gradient_to_color: Optional[str] = None, theme_id: Optional[str] = None ) -> str: """Create a new course in Super Singularity. NOTE: All courses created via MCP server are automatically marked as AI-generated. Args: title: Course title (max 255 characters) duration: Course duration in minutes (default: 0) description: Course description (max 1000 characters) folder_id: ID of the folder to organize the course finalized_course_plan: Course plan in Markdown format (max 13000 chars) - stores original prompt and structure is_published: Whether the course is visible to learners (default: false) is_autoplay: Auto-advance cards (default: false) is_scorable: Track quiz scores (default: false) gradient_from_color: Hex color for gradient start (e.g., "#FF0000") gradient_to_color: Hex color for gradient end theme_id: Custom theme ID """ course_data = { "id": generate_uuid(), "title": title, "companyId": COMPANY_ID, "duration": duration, "isPublished": parse_bool(is_published) or False, "isAutoplay": parse_bool(is_autoplay) or False, "isScorable": parse_bool(is_scorable) or False, "createdByAgent": True # Always true for MCP server created courses } # Handle string duration if passed if isinstance(duration, str): try: course_data["duration"] = int(duration) except ValueError: course_data["duration"] = 0 # Add optional fields if provided if description: course_data["description"] = description if folder_id: course_data["folderId"] = folder_id if finalized_course_plan: course_data["finalizedCoursePlan"] = finalized_course_plan if gradient_from_color: course_data["gradientFromColor"] = gradient_from_color if gradient_to_color: course_data["gradientToColor"] = gradient_to_color if theme_id: course_data["themeId"] = theme_id result = make_api_request("POST", "/api/createCourse", course_data) return json.dumps(result, indent=2) @mcp.tool() def create_audio_card( course_id: str, audio_url: str, title: str, background_image_url: Optional[str] = None, audio_script: Optional[str] = None, audio_generated: Optional[Union[bool, str]] = None, audio_generated_at: Optional[str] = None, image_prompt: Optional[str] = None, image_generated: Optional[Union[bool, str]] = None, image_generated_at: Optional[str] = None, sort_order: Optional[Union[int, str]] = None, is_mandatory: Union[bool, str] = False ) -> str: """Create an audio card with existing audio URL. Args: course_id: The course to add this card to audio_url: URL of the audio file (MP3, WAV, OGG) title: Title text for the audio card background_image_url: Optional background image URL audio_script: Optional script text that was used to generate the audio audio_generated: Optional flag to indicate if audio was generated (true) or uploaded audio_generated_at: Optional timestamp when audio was generated (ISO string, IST) image_prompt: Optional original image generation prompt for background image image_generated: Optional flag to indicate if background image was generated image_generated_at: Optional timestamp when background image was generated sort_order: Position in course (auto-incremented if not provided) is_mandatory: Whether learner must listen to proceed """ # Helper to parse boolean strings def parse_bool(val: Union[bool, str, None]) -> Optional[bool]: if val is None: return None if isinstance(val, bool): return val if isinstance(val, str): return val.lower() == "true" return False # Parse boolean fields audio_generated_bool = parse_bool(audio_generated) image_generated_bool = parse_bool(image_generated) is_mandatory_bool = parse_bool(is_mandatory) or False contents = { "_header1": { "text": title, "visibility": True, "size": "medium" }, "header1": title, "audio": audio_url } if background_image_url: contents["image"] = background_image_url if audio_script: contents["audioScript"] = audio_script if audio_generated_bool is not None: contents["audioGenerated"] = audio_generated_bool if audio_generated_at: contents["audioGeneratedAt"] = audio_generated_at # Always set audioGeneratedBy to CLAUDE_MCP_SERVER when audio is generated if audio_generated_bool: contents["audioGeneratedBy"] = "CLAUDE_MCP_SERVER" # Add image generation tracking fields for background image if provided if image_prompt: contents["imagePrompt"] = image_prompt if image_generated_bool is not None: contents["imageGenerated"] = image_generated_bool if image_generated_at: contents["imageGeneratedAt"] = image_generated_at # Always set imageGeneratedBy to CLAUDE_MCP_SERVER when background image is generated if image_generated_bool: contents["imageGeneratedBy"] = "CLAUDE_MCP_SERVER" card_data = { "courseId": course_id, "cardType": "audio", "contents": contents, "isMandatory": is_mandatory_bool } if sort_order: if isinstance(sort_order, str): try: card_data["sortOrder"] = int(sort_order) except ValueError: pass else: card_data["sortOrder"] = sort_order result = make_api_request("POST", "/api/createCard", card_data) return json.dumps(result, indent=2) @mcp.tool() def generate_background_image_for_audio( prompt: str, title: str ) -> str: """Generate a background image specifically for audio cards using Azure OpenAI. USE THIS TOOL FOR: Audio card background images ONLY. FOR CONTENT CARD IMAGES: Use generate_image_from_text instead (flexible aspect ratios). NOTE: Audio card background images are ALWAYS generated in portrait format (1024x1536) for optimal display. Args: prompt: Detailed prompt for background image generation title: Title for the image file """ try: # Generate timestamp in IST ist = pytz.timezone('Asia/Kolkata') generated_at = datetime.now(ist).isoformat() # ALWAYS use portrait size for audio card backgrounds size = "1024x1536" # Generate and upload image image_url = generate_and_upload_image(prompt, title, size, "png") return f"""Background image generated and uploaded successfully! Image URL: {image_url} Image Format: Portrait (1024x1536) - optimized for audio cards IMPORTANT: When creating the audio card with this background image, include these parameters: - background_image_url: "{image_url}" - image_prompt: "{prompt}" - image_generated: true - image_generated_at: "{generated_at}" Use create_audio_card with all audio parameters PLUS these image tracking parameters for the background. Note: The system automatically sets imageGeneratedBy to 'CLAUDE_MCP_SERVER' when image_generated is true.""" except Exception as e: error_msg = str(e) logger.error(f"Background image generation failed: {error_msg}") fallback_url = "https://placehold.co/1024x1536/png?text=Image+Generation+Failed" return f"""⚠️ Background image generation failed! Error: {error_msg} Using fallback image so you can proceed. Image URL: {fallback_url} Image Format: Portrait (1024x1536) - optimized for audio cards IMPORTANT: When creating the audio card with this background image, include these parameters: - background_image_url: "{fallback_url}" - image_prompt: "{prompt}" - image_generated: true - image_generated_at: "{generated_at}" Use create_audio_card with all audio parameters PLUS these image tracking parameters for the background.""" @mcp.tool() async def echo_message(message: str) -> str: """Echo back a message for testing. Args: message: The message to echo back """ return f"Echo: {message}" @mcp.tool() def generate_audio_from_text(text: str, title: str) -> str: """Generate audio from text using ElevenLabs TTS and upload to Azure Storage. Args: text: Text to convert to speech title: Title for the audio file """ try: # Generate timestamp in IST ist = pytz.timezone('Asia/Kolkata') generated_at = datetime.now(ist).isoformat() audio_url = generate_and_upload_audio(text, title) return f"""Audio generated and uploaded successfully! Audio URL: {audio_url} IMPORTANT: When creating the audio card, include these parameters to properly track generated audio: - script: "{text}" - audio_generated: true - audio_generated_at: "{generated_at}" Use create_audio_card with script, audio_generated, and audio_generated_at parameters to maintain proper tracking. If you're also generating a background image for this audio card, remember to include the image tracking parameters as well. Note: For audio card background images, consider using generate_background_image_for_audio which automatically generates portrait-format images optimized for audio cards.""" except Exception as e: return f"Error: {str(e)}" @mcp.tool() def generate_image_from_text( prompt: str, title: str, aspect_ratio: Optional[str] = None, output_format: Optional[str] = None ) -> str: """Generate image from text prompt using Azure OpenAI and upload to Azure Storage. USE THIS TOOL FOR: Content card images (any aspect ratio). FOR AUDIO CARD BACKGROUNDS: Use generate_background_image_for_audio instead (optimized portrait format). Args: prompt: Detailed prompt for image generation (mandatory) title: Title for the image file (mandatory) aspect_ratio: Image aspect ratio ("square", "portrait", or "landscape", optional, defaults to "square") output_format: Original format from OpenAI ("png" or "jpg", optional, defaults to "png") """ try: # Generate timestamp in IST ist = pytz.timezone('Asia/Kolkata') generated_at = datetime.now(ist).isoformat() # Set defaults if aspect_ratio is None: aspect_ratio = "square" if output_format is None: output_format = "png" # Map aspect ratio to size if aspect_ratio.lower() == "square": size = "1024x1024" elif aspect_ratio.lower() == "portrait": size = "1024x1536" elif aspect_ratio.lower() == "landscape": size = "1536x1024" else: # Default to square if invalid aspect ratio provided size = "1024x1024" # Validate output format if output_format not in ["png", "jpg"]: return f"Error: Invalid output format '{output_format}'. Must be 'png' or 'jpg'" # Generate and upload image image_url = generate_and_upload_image(prompt, title, size, output_format) return f"""Image generated and uploaded successfully! Image URL: {image_url} IMPORTANT: When creating content or audio cards with this image, include these parameters to properly track generated images: - image_prompt: "{prompt}" - image_generated: true - image_generated_at: "{generated_at}" For content cards: Use create_content_card with image_url, image_prompt, image_generated, and image_generated_at parameters. For audio cards (background image): Use create_audio_card with background_image_url, image_prompt, image_generated, and image_generated_at parameters. Note: The system automatically sets imageGeneratedBy to 'CLAUDE_MCP_SERVER' when image_generated is true.""" except Exception as e: error_msg = str(e) logger.error(f"Image generation failed: {error_msg}") fallback_url = "https://placehold.co/1024x1024/png?text=Image+Generation+Failed" return f"""⚠️ Image generation failed! Error: {error_msg} Using fallback image so you can proceed. Image URL: {fallback_url} IMPORTANT: When creating content or audio cards with this image, include these parameters to properly track generated images: - image_prompt: "{prompt}" - image_generated: true - image_generated_at: "{generated_at}" For content cards: Use create_content_card with image_url, image_prompt, image_generated, and image_generated_at parameters. For audio cards (background image): Use create_audio_card with background_image_url, image_prompt, image_generated, and image_generated_at parameters.""" @mcp.tool() def get_card(card_id: str) -> str: """Get details of a specific card. Args: card_id: The ID of the card to retrieve """ result = make_api_request("GET", f"/api/card/{card_id}") return json.dumps(result, indent=2) @mcp.tool() def get_course_cards(course_id: str) -> str: """Get all cards for a specific course. Args: course_id: The ID of the course to get cards for """ result = make_api_request("GET", f"/api/courses/{course_id}/cards") return json.dumps(result, indent=2) @mcp.tool() def create_content_card( course_id: str, header1_text: str, header2_text: Optional[str] = None, image_url: Optional[str] = None, image_prompt: Optional[str] = None, image_generated: Optional[Union[bool, str]] = None, image_generated_at: Optional[str] = None, align: str = "center center", sort_order: Optional[Union[int, str]] = None, is_mandatory: Union[bool, str] = False ) -> str: """Create a content card with text and optional image. Args: course_id: The course to add this card to header1_text: Main heading text (supports HTML formatting) header2_text: Secondary text or description image_url: Optional image URL image_prompt: Optional original image generation prompt image_generated: Optional flag to indicate if image was generated image_generated_at: Optional timestamp when image was generated (ISO string) align: Content alignment ("center center", "top", "bottom", or "bg") sort_order: Position in course (auto-incremented if not provided) is_mandatory: Whether card is mandatory to view """ contents = { "_header1": { "text": header1_text, "visibility": True, "size": "medium" }, "header1": header1_text.replace("<b>", "").replace("</b>", "").replace("<i>", "").replace("</i>", "") } if header2_text: contents["_header2"] = { "text": header2_text, "visibility": True, "size": "medium" } contents["header2"] = header2_text.replace("<b>", "").replace("</b>", "").replace("<i>", "").replace("</i>", "") if image_url: contents["image"] = image_url contents["align"] = align # Add image generation tracking fields if provided if image_prompt: contents["imagePrompt"] = image_prompt image_generated_bool = parse_bool(image_generated) if image_generated_bool is not None: contents["imageGenerated"] = image_generated_bool if image_generated_at: contents["imageGeneratedAt"] = image_generated_at # Always set imageGeneratedBy to CLAUDE_MCP_SERVER when image is generated if image_generated_bool: contents["imageGeneratedBy"] = "CLAUDE_MCP_SERVER" card_data = { "courseId": course_id, "cardType": "content", "contents": contents, "align": align, "isMandatory": parse_bool(is_mandatory) or False } if sort_order: if isinstance(sort_order, str): try: card_data["sortOrder"] = int(sort_order) except ValueError: pass else: card_data["sortOrder"] = sort_order result = make_api_request("POST", "/api/createCard", card_data) return json.dumps(result, indent=2) @mcp.tool() def create_quiz_card( course_id: str, question: str, options: List[str], correct_answer: str, comment: Optional[str] = None, sort_order: Optional[Union[int, str]] = None, is_mandatory: Union[bool, str] = True ) -> str: """Create a quiz card with multiple choice question. Args: course_id: The course to add this card to question: The quiz question options: List of 2-4 answer options correct_answer: The correct answer (must match one of the options exactly) comment: Optional explanation for the answer sort_order: Position in course (auto-incremented if not provided) is_mandatory: Whether learner must answer to proceed (default: true) """ if len(options) < 2 or len(options) > 4: return json.dumps({"error": "Quiz must have 2-4 options"}, indent=2) if correct_answer not in options: return json.dumps({"error": f"Correct answer '{correct_answer}' must be one of the provided options"}, indent=2) contents = { "_header1": { "text": question, "visibility": True, "size": "medium" }, "header1": question, "options": options, "correct": [correct_answer] } if comment: contents["comment"] = comment card_data = { "courseId": course_id, "cardType": "quiz", "contents": contents, "isMandatory": parse_bool(is_mandatory) or True } if sort_order: if isinstance(sort_order, str): try: card_data["sortOrder"] = int(sort_order) except ValueError: pass else: card_data["sortOrder"] = sort_order result = make_api_request("POST", "/api/createCard", card_data) return json.dumps(result, indent=2) @mcp.tool() def create_poll_card( course_id: str, question: str, options: List[str], sort_order: Optional[Union[int, str]] = None, is_mandatory: Union[bool, str] = False ) -> str: """Create a poll card for collecting learner opinions. Args: course_id: The course to add this card to question: The poll question options: List of 2-4 poll options sort_order: Position in course (auto-incremented if not provided) is_mandatory: Whether learner must respond to proceed """ if len(options) < 2 or len(options) > 4: return json.dumps({"error": "Poll must have 2-4 options"}, indent=2) contents = { "_header1": { "text": question, "visibility": True, "size": "medium" }, "options": options } card_data = { "courseId": course_id, "cardType": "poll", "contents": contents, "isMandatory": parse_bool(is_mandatory) or False } if sort_order: if isinstance(sort_order, str): try: card_data["sortOrder"] = int(sort_order) except ValueError: pass else: card_data["sortOrder"] = sort_order result = make_api_request("POST", "/api/createCard", card_data) return json.dumps(result, indent=2) @mcp.tool() def create_form_card( course_id: str, question: str, sort_order: Optional[Union[int, str]] = None, is_mandatory: Union[bool, str] = False ) -> str: """Create a form card for collecting learner input. Args: course_id: The course to add this card to question: The form question/prompt sort_order: Position in course (auto-incremented if not provided) is_mandatory: Whether learner must respond to proceed """ contents = { "_header1": { "text": question, "visibility": True, "size": "medium" } } card_data = { "courseId": course_id, "cardType": "form", "contents": contents, "isMandatory": parse_bool(is_mandatory) or False } if sort_order: if isinstance(sort_order, str): try: card_data["sortOrder"] = int(sort_order) except ValueError: pass else: card_data["sortOrder"] = sort_order result = make_api_request("POST", "/api/createCard", card_data) return json.dumps(result, indent=2) @mcp.tool() def create_video_card( course_id: str, video_url: str, sort_order: Optional[Union[int, str]] = None, is_mandatory: Union[bool, str] = False ) -> str: """Create a video card for video content. Args: course_id: The course to add this card to video_url: URL of the video file (MP4, WebM) sort_order: Position in course (auto-incremented if not provided) is_mandatory: Whether learner must watch to proceed """ contents = { "video": video_url } card_data = { "courseId": course_id, "cardType": "video", "contents": contents, "isMandatory": parse_bool(is_mandatory) or False } if sort_order: if isinstance(sort_order, str): try: card_data["sortOrder"] = int(sort_order) except ValueError: pass else: card_data["sortOrder"] = sort_order result = make_api_request("POST", "/api/createCard", card_data) return json.dumps(result, indent=2) @mcp.tool() def create_link_card( course_id: str, title: str, link_url: str, link_caption: str = "Visit Link", sort_order: Optional[Union[int, str]] = None ) -> str: """Create a link card for external resources. Args: course_id: The course to add this card to title: Title text for the link card link_url: URL of the external resource link_caption: Text for the link button (default: "Visit Link") sort_order: Position in course (auto-incremented if not provided) """ contents = { "_header1": { "text": title, "visibility": True, "size": "medium" }, "header1": title, "link": link_url, "linkcaption": link_caption } card_data = { "courseId": course_id, "cardType": "link", "contents": contents } if sort_order: if isinstance(sort_order, str): try: card_data["sortOrder"] = int(sort_order) except ValueError: pass else: card_data["sortOrder"] = sort_order result = make_api_request("POST", "/api/createCard", card_data) return json.dumps(result, indent=2) @mcp.tool() def update_card( card_id: str, contents: Optional[Union[Dict, str]] = None, is_mandatory: Optional[Union[bool, str]] = None, sort_order: Optional[Union[int, str]] = None, is_active: Optional[Union[bool, str]] = None, card_type: Optional[str] = None ) -> str: """Update an existing card with automatic preservation of AI-generated metadata. IMPORTANT: When updating contents, this tool automatically fetches the current card and merges your updates to preserve existing AI metadata (imagePrompt, audioScript, etc.). Args: card_id: The ID of the card to update contents: Partial contents updates to merge with existing (preserves AI metadata) is_mandatory: Whether the card is mandatory sort_order: Position in course is_active: Whether the card is active card_type: Change card type (WARNING: triggers validation, may remove fields) """ update_data = {} # Handle contents update with GET-merge-PUT to preserve AI metadata if contents is not None: # Fetch current card to preserve existing fields current_result = make_api_request("GET", f"/api/card/{card_id}") if "error" in current_result: return json.dumps({"error": f"Failed to fetch card for update: {current_result['error']}"}, indent=2) # Merge contents (shallow merge - preserves all top-level keys) current_contents = current_result.get("contents", {}) # Handle string contents (parse JSON) contents_to_merge = contents if isinstance(contents, str): try: contents_to_merge = json.loads(contents) except json.JSONDecodeError: return json.dumps({"error": "Invalid JSON string provided for contents"}, indent=2) merged_contents = { **current_contents, # Preserve all existing fields **contents_to_merge # Apply updates } update_data["contents"] = merged_contents # Add other fields if provided is_mandatory_bool = parse_bool(is_mandatory) if is_mandatory_bool is not None: update_data["isMandatory"] = is_mandatory_bool if sort_order is not None: if isinstance(sort_order, str): try: update_data["sortOrder"] = int(sort_order) except ValueError: pass else: update_data["sortOrder"] = sort_order is_active_bool = parse_bool(is_active) if is_active_bool is not None: update_data["isActive"] = is_active_bool # Only include cardType if explicitly changing it # WARNING: This triggers validation which may clean/remove AI metadata fields if card_type is not None: update_data["cardType"] = card_type # Note: Changing card type may cause validation to remove some fields if not update_data: return json.dumps({"error": "No update data provided"}, indent=2) result = make_api_request("PUT", f"/api/card/{card_id}", update_data) return json.dumps(result, indent=2) @mcp.tool() async def get_server_info() -> str: """Get basic information about this MCP server.""" return "Super Singularity MCP Server v1.0 - Complete course and card creation with ElevenLabs TTS + Azure Storage"

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/Ishant-93/MCP'

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