xkcd-mcp-server / app.py
gnumanth's picture
feat: more tools
69ba8d5 verified
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)