import logging
import io
import base64
from typing import Optional, Sequence, List
import threading
import osxphotos
from osxphotos import PhotoInfo
from datetime import datetime
from mcp.server.fastmcp import FastMCP
from mcp.types import TextContent, ImageContent
from PIL import Image, ImageDraw, ImageFont
from collections import Counter
class PhotosDBLoader:
def __init__(self):
self._db: Optional[osxphotos.PhotosDB] = None
self.load_db()
def load_db(self):
def load():
try:
self._db = osxphotos.PhotosDB()
logging.info("Loaded PhotosDB 📸")
except Exception as e:
logging.error(f"Error: Could not load PhotosDB {e}")
thread = threading.Thread(target=load)
thread.daemon = True
thread.start()
@property
def db(self) -> osxphotos.PhotosDB:
if self._db is None:
logging.warning("PhotosDB is still loading; access attempted.")
raise Exception("PhotosDB is still loading. Please try again later.")
return self._db
# Global PhotosDB loader instance
photos_loader = PhotosDBLoader()
mcp = FastMCP("year-wrapped")
def generate_image(text: str) -> str:
img = Image.open("./bg.png").convert("RGBA")
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("InstrumentSans.ttf", size=70)
bbox = draw.textbbox((0,0), text, font=font)
text_width = bbox[2]- bbox[0]
text_height = bbox[3] - bbox[1]
OFFSET_X = 170 # move right
OFFSET_Y = -100 # move up
img_width, img_height = img.size
x = (img_width - text_width) / 2 + OFFSET_X
y = (img_height - text_height) / 2 + OFFSET_Y
draw.text((x, y), text, font=font, fill="white")
buffer = io.BytesIO()
img.save(buffer, format="WEBP", lossless=True)
buffer.seek(0)
# Encode to base64
return base64.b64encode(buffer.read()).decode("utf-8")
def all_photos_current_year() -> List[PhotoInfo]:
current_year = datetime.now().year
start = datetime(current_year, 1, 1)
end = datetime(current_year, 12, 31, 23, 59, 59)
return photos_loader.db.photos(from_date=start, to_date=end)
def find_top_date() -> list[str | int]:
# Date on which you took most photos
dates = [p.date.date() for p in all_photos_current_year() if p.date]
counts = Counter(dates)
if not counts:
print("No photos with a date found.")
else:
top_date, top_count = counts.most_common(1)[0]
return [f"{top_date.isoformat()}", top_count]
return ["", ""]
@mcp.tool()
async def get_year_wrapped() -> Sequence[TextContent | ImageContent]:
"""Get Year Wrapped of the photo library,
This returns an image with statistics about the photos you clicked this year.
"""
# Total Photos in this year
total_photos = len(all_photos_current_year())
top_date, top_count = find_top_date()
text = f"You clicked {total_photos} photos!"
if top_date != "":
text += f"\n\n On {top_date} you clicked {top_count} photos! \n That was your top photos day!"
img = generate_image(text)
return [
ImageContent(
type="image",
data=img,
mimeType="image/webp"
)]
def main():
# Initialize and run the server
mcp.run(transport='stdio')
if __name__ == "__main__":
main()