import gradio as gr import requests import json import random from typing import Dict, Any, Optional def get_latest() -> str: """ Fetch the latest XKCD comic. Returns: str: JSON string containing latest comic information """ try: url = "https://xkcd.com/info.0.json" response = requests.get(url, timeout=10) response.raise_for_status() comic_data = response.json() formatted_response = { "num": comic_data["num"], "title": comic_data["title"], "alt": comic_data["alt"], "img": comic_data["img"], "year": comic_data["year"], "month": comic_data["month"], "day": comic_data["day"], "transcript": comic_data.get("transcript", ""), "safe_title": comic_data["safe_title"] } return json.dumps(formatted_response, indent=2) except requests.exceptions.RequestException as e: return f"Error fetching latest comic: {str(e)}" except KeyError as e: return f"Error parsing comic data: Missing field {str(e)}" except Exception as e: return f"Unexpected error: {str(e)}" def get_comic(comic_id: str) -> str: """ Fetch a specific XKCD comic by ID. Args: comic_id (str): Comic ID number Returns: str: JSON string containing comic information """ try: if not comic_id.strip(): return "Error: Comic ID is required" url = f"https://xkcd.com/{comic_id.strip()}/info.0.json" response = requests.get(url, timeout=10) response.raise_for_status() comic_data = response.json() formatted_response = { "num": comic_data["num"], "title": comic_data["title"], "alt": comic_data["alt"], "img": comic_data["img"], "year": comic_data["year"], "month": comic_data["month"], "day": comic_data["day"], "transcript": comic_data.get("transcript", ""), "safe_title": comic_data["safe_title"] } return json.dumps(formatted_response, indent=2) except requests.exceptions.RequestException as e: return f"Error fetching comic {comic_id}: {str(e)}" except KeyError as e: return f"Error parsing comic data: Missing field {str(e)}" except Exception as e: return f"Unexpected error: {str(e)}" def get_random() -> str: """ Fetch a random XKCD comic. Returns: str: JSON string containing random comic information """ try: # First get the latest comic to know the range latest_response = requests.get("https://xkcd.com/info.0.json", timeout=10) latest_response.raise_for_status() latest_num = latest_response.json()["num"] # Generate random comic ID (excluding 404 which doesn't exist) comic_id = random.randint(1, latest_num) if comic_id == 404: comic_id = 405 url = f"https://xkcd.com/{comic_id}/info.0.json" response = requests.get(url, timeout=10) response.raise_for_status() comic_data = response.json() formatted_response = { "num": comic_data["num"], "title": comic_data["title"], "alt": comic_data["alt"], "img": comic_data["img"], "year": comic_data["year"], "month": comic_data["month"], "day": comic_data["day"], "transcript": comic_data.get("transcript", ""), "safe_title": comic_data["safe_title"] } return json.dumps(formatted_response, indent=2) except requests.exceptions.RequestException as e: return f"Error fetching random comic: {str(e)}" except KeyError as e: return f"Error parsing comic data: Missing field {str(e)}" except Exception as e: return f"Unexpected error: {str(e)}" def search_xkcd_transcript(search_term: str) -> str: """ Search for XKCD comics by searching their transcripts and titles. Note: This is a simple demonstration - in a real implementation you'd want a proper search index. Args: search_term (str): Term to search for in comic transcripts and titles Returns: str: JSON string containing matching comics information """ try: # Get latest comic number first latest_response = requests.get("https://xkcd.com/info.0.json", timeout=10) latest_response.raise_for_status() latest_num = latest_response.json()["num"] matches = [] search_term_lower = search_term.lower() # Search through comics that are more likely to have transcripts (1-500 range for faster results) # Recent comics often don't have transcripts, so we search older ones first max_search_range = min(500, latest_num) for comic_num in range(1, max_search_range + 1): try: url = f"https://xkcd.com/{comic_num}/info.0.json" response = requests.get(url, timeout=2) response.raise_for_status() comic_data = response.json() # Check if search term is in title, alt text, safe_title, or transcript if (search_term_lower in comic_data["title"].lower() or search_term_lower in comic_data["alt"].lower() or search_term_lower in comic_data.get("safe_title", "").lower() or search_term_lower in comic_data.get("transcript", "").lower()): matches.append({ "num": comic_data["num"], "title": comic_data["title"], "alt": comic_data["alt"][:100] + "..." if len(comic_data["alt"]) > 100 else comic_data["alt"], "img": comic_data["img"] }) # Limit results to prevent long search times if len(matches) >= 10: break except: continue # Skip comics that can't be fetched return json.dumps({"search_term": search_term, "matches": matches}, indent=2) except Exception as e: return f"Search error: {str(e)}" # Helper function for the Gradio interface def get_xkcd_comic(comic_id: str = "") -> str: """Wrapper function for Gradio interface compatibility""" if not comic_id.strip(): return get_latest() else: return get_comic(comic_id) # Create Gradio interface with gr.Blocks(title="XKCD MCP Server") as demo: gr.Markdown("# XKCD MCP Server") gr.Markdown("This server provides tools to fetch and search XKCD comics via MCP protocol.") with gr.Tab("Get Comic"): with gr.Row(): with gr.Column(): latest_btn = gr.Button("Get Latest Comic") random_btn = gr.Button("Get Random Comic") with gr.Column(): comic_input = gr.Textbox( label="Comic ID", placeholder="Enter comic ID number", value="" ) specific_btn = gr.Button("Get Specific Comic") comic_output = gr.Textbox( label="Comic Data (JSON)", lines=15 ) latest_btn.click(get_latest, outputs=[comic_output]) random_btn.click(get_random, outputs=[comic_output]) specific_btn.click(get_comic, inputs=[comic_input], outputs=[comic_output]) with gr.Tab("Search Comics"): search_input = gr.Textbox( label="Search Term", placeholder="Enter term to search in titles, alt text, and transcripts" ) search_output = gr.Textbox( label="Search Results (JSON)", lines=15 ) search_btn = gr.Button("Search") search_btn.click(search_xkcd_transcript, inputs=[search_input], outputs=[search_output]) if __name__ == "__main__": demo.launch(mcp_server=True, share=True)