Skip to main content
Glama

MCP-DayOne

by Quevin
server.py20.6 kB
"""MCP server for Day One Journal integration.""" import asyncio import logging from typing import Any, Sequence from mcp.server import Server from mcp.server.models import InitializationOptions from mcp.server.stdio import stdio_server from mcp.types import ( ServerCapabilities, TextContent, Tool, ToolsCapability, ) from pydantic import BaseModel, Field from .tools import DayOneTools, DayOneError # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Tool argument models class CreateEntryArgs(BaseModel): content: str = Field(description="The text content of the journal entry") tags: list[str] = Field(default=[], description="Optional list of tags for the entry") date: str = Field(default="", description="Optional date in YYYY-MM-DD HH:MM:SS format") journal: str = Field(default="", description="Optional journal name") attachments: list[str] = Field(default=[], description="Optional list of file paths to attach (max 10)") starred: bool = Field(default=False, description="Mark entry as starred/important") coordinates: dict[str, float] = Field(default={}, description="Optional coordinates with 'latitude' and 'longitude' keys") timezone: str = Field(default="", description="Optional timezone (e.g., 'America/New_York')") all_day: bool = Field(default=False, description="Mark as all-day event") class ListJournalsArgs(BaseModel): pass class GetEntryCountArgs(BaseModel): journal: str = Field(default="", description="Optional journal name to count entries for") class CreateEntryWithAttachmentsArgs(BaseModel): content: str = Field(description="The text content of the journal entry") attachments: list[str] = Field(description="List of file paths to attach (max 10)") tags: list[str] = Field(default=[], description="Optional list of tags for the entry") journal: str = Field(default="", description="Optional journal name") starred: bool = Field(default=False, description="Mark entry as starred/important") class CreateLocationEntryArgs(BaseModel): content: str = Field(description="The text content of the journal entry") latitude: float = Field(description="Latitude coordinate") longitude: float = Field(description="Longitude coordinate") tags: list[str] = Field(default=[], description="Optional list of tags for the entry") journal: str = Field(default="", description="Optional journal name") starred: bool = Field(default=False, description="Mark entry as starred/important") class ReadRecentEntriesArgs(BaseModel): limit: int = Field(default=10, description="Maximum number of entries to return (1-50)") journal: str = Field(default="", description="Optional journal name to filter by") class SearchEntriesArgs(BaseModel): search_text: str = Field(description="Text to search for in entry content") limit: int = Field(default=20, description="Maximum number of entries to return (1-50)") journal: str = Field(default="", description="Optional journal name to filter by") class ListJournalsFromDbArgs(BaseModel): pass class GetEntryCountFromDbArgs(BaseModel): journal: str = Field(default="", description="Optional journal name to count entries for") class GetEntriesByDateArgs(BaseModel): target_date: str = Field(description="Target date in MM-DD or YYYY-MM-DD format (e.g., '06-14' for June 14th)") years_back: int = Field(default=5, description="How many years back to search (default 5)") # Global Day One tools instance dayone_tools: DayOneTools = None def get_available_tools() -> list[Tool]: """Get list of available MCP tools.""" return [ Tool( name="create_journal_entry", description="Create a new entry in Day One journal with support for attachments, location, and metadata", inputSchema=CreateEntryArgs.model_json_schema(), ), Tool( name="list_journals", description="List all available Day One journals", inputSchema=ListJournalsArgs.model_json_schema(), ), Tool( name="get_entry_count", description="Get the total number of entries in a journal", inputSchema=GetEntryCountArgs.model_json_schema(), ), Tool( name="create_entry_with_attachments", description="Create a journal entry with file attachments (photos, videos, audio, PDFs)", inputSchema=CreateEntryWithAttachmentsArgs.model_json_schema(), ), Tool( name="create_location_entry", description="Create a journal entry with location coordinates", inputSchema=CreateLocationEntryArgs.model_json_schema(), ), Tool( name="read_recent_entries", description="Read recent journal entries from Day One database", inputSchema=ReadRecentEntriesArgs.model_json_schema(), ), Tool( name="search_entries", description="Search journal entries by text content", inputSchema=SearchEntriesArgs.model_json_schema(), ), Tool( name="list_journals_from_db", description="List all journals from database with entry counts", inputSchema=ListJournalsFromDbArgs.model_json_schema(), ), Tool( name="get_entry_count_from_db", description="Get actual entry count from Day One database", inputSchema=GetEntryCountFromDbArgs.model_json_schema(), ), Tool( name="get_entries_by_date", description="Get journal entries for a specific date across multiple years ('On This Day' feature)", inputSchema=GetEntriesByDateArgs.model_json_schema(), ), ] async def handle_create_journal_entry(args: CreateEntryArgs) -> list[TextContent]: """Handle creating a new journal entry.""" try: uuid = dayone_tools.create_entry( content=args.content, tags=args.tags if args.tags else None, date=args.date if args.date else None, journal=args.journal if args.journal else None, attachments=args.attachments if args.attachments else None, starred=args.starred if args.starred else None, coordinates=args.coordinates if args.coordinates else None, timezone=args.timezone if args.timezone else None, all_day=args.all_day if args.all_day else None, ) return [TextContent( type="text", text=f"Successfully created journal entry with UUID: {uuid}" )] except DayOneError as e: return [TextContent( type="text", text=f"Error creating journal entry: {str(e)}" )] async def handle_list_journals(args: ListJournalsArgs) -> list[TextContent]: """Handle listing available journals.""" try: messages = dayone_tools.list_journals() journal_info = "\n".join(f"• {message}" for message in messages) return [TextContent( type="text", text=f"Journal Information:\n{journal_info}" )] except DayOneError as e: return [TextContent( type="text", text=f"Error getting journal information: {str(e)}" )] async def handle_get_entry_count(args: GetEntryCountArgs) -> list[TextContent]: """Handle getting entry count.""" try: count = dayone_tools.get_entry_count( journal=args.journal if args.journal else None ) journal_text = f" in journal '{args.journal}'" if args.journal else "" return [TextContent( type="text", text=f"Total entries{journal_text}: {count}" )] except DayOneError as e: return [TextContent( type="text", text=f"Entry count limitation: {str(e)}" )] async def handle_create_entry_with_attachments(args: CreateEntryWithAttachmentsArgs) -> list[TextContent]: """Handle creating a journal entry with attachments.""" try: uuid = dayone_tools.create_entry( content=args.content, attachments=args.attachments, tags=args.tags if args.tags else None, journal=args.journal if args.journal else None, starred=args.starred if args.starred else None, ) attachment_count = len(args.attachments) return [TextContent( type="text", text=f"Successfully created journal entry with {attachment_count} attachment(s). UUID: {uuid}" )] except DayOneError as e: return [TextContent( type="text", text=f"Error creating entry with attachments: {str(e)}" )] async def handle_create_location_entry(args: CreateLocationEntryArgs) -> list[TextContent]: """Handle creating a journal entry with location.""" try: coordinates = { "latitude": args.latitude, "longitude": args.longitude } uuid = dayone_tools.create_entry( content=args.content, coordinates=coordinates, tags=args.tags if args.tags else None, journal=args.journal if args.journal else None, starred=args.starred if args.starred else None, ) return [TextContent( type="text", text=f"Successfully created location entry at {args.latitude}, {args.longitude}. UUID: {uuid}" )] except DayOneError as e: return [TextContent( type="text", text=f"Error creating location entry: {str(e)}" )] async def handle_read_recent_entries(args: ReadRecentEntriesArgs) -> list[TextContent]: """Handle reading recent journal entries.""" try: # Validate limit limit = max(1, min(50, args.limit)) entries = dayone_tools.read_recent_entries( limit=limit, journal=args.journal if args.journal else None ) if not entries: return [TextContent( type="text", text="No entries found." )] # Format entries for display result_lines = [f"Found {len(entries)} recent entries:\n"] for i, entry in enumerate(entries, 1): date_str = entry['creation_date'].strftime("%Y-%m-%d %H:%M") if entry['creation_date'] else "Unknown date" starred_str = " ⭐" if entry['starred'] else "" tags_str = f" #{' #'.join(entry['tags'])}" if entry['tags'] else "" # Truncate text for preview text_preview = entry['text'][:100] + "..." if len(entry['text']) > 100 else entry['text'] result_lines.append( f"{i}. {date_str}{starred_str} [{entry['journal_name']}]\n" f" {text_preview}{tags_str}\n" ) return [TextContent( type="text", text="\n".join(result_lines) )] except DayOneError as e: return [TextContent( type="text", text=f"Error reading entries: {str(e)}" )] async def handle_search_entries(args: SearchEntriesArgs) -> list[TextContent]: """Handle searching journal entries.""" try: # Validate limit limit = max(1, min(50, args.limit)) entries = dayone_tools.search_entries( search_text=args.search_text, limit=limit, journal=args.journal if args.journal else None ) if not entries: return [TextContent( type="text", text=f"No entries found matching '{args.search_text}'." )] # Format search results result_lines = [f"Found {len(entries)} entries matching '{args.search_text}':\n"] for i, entry in enumerate(entries, 1): date_str = entry['creation_date'].strftime("%Y-%m-%d %H:%M") if entry['creation_date'] else "Unknown date" starred_str = " ⭐" if entry['starred'] else "" tags_str = f" #{' #'.join(entry['tags'])}" if entry['tags'] else "" # Show more context for search results text_preview = entry['text'][:200] + "..." if len(entry['text']) > 200 else entry['text'] result_lines.append( f"{i}. {date_str}{starred_str} [{entry['journal_name']}]\n" f" {text_preview}{tags_str}\n" ) return [TextContent( type="text", text="\n".join(result_lines) )] except DayOneError as e: return [TextContent( type="text", text=f"Error searching entries: {str(e)}" )] async def handle_list_journals_from_db(args: ListJournalsFromDbArgs) -> list[TextContent]: """Handle listing journals from database.""" try: journals = dayone_tools.list_journals_from_db() if not journals: return [TextContent( type="text", text="No journals found in database." )] # Format journal list result_lines = ["Your Day One Journals:\n"] for journal in journals: last_entry_str = "" if journal['last_entry_date']: last_entry_str = f" (last entry: {journal['last_entry_date'].strftime('%Y-%m-%d')})" result_lines.append( f"• {journal['name']}: {journal['entry_count']} entries{last_entry_str}" ) total_entries = sum(j['entry_count'] for j in journals) result_lines.append(f"\nTotal: {len(journals)} journals, {total_entries} entries") return [TextContent( type="text", text="\n".join(result_lines) )] except DayOneError as e: return [TextContent( type="text", text=f"Error listing journals: {str(e)}" )] async def handle_get_entry_count_from_db(args: GetEntryCountFromDbArgs) -> list[TextContent]: """Handle getting entry count from database.""" try: count = dayone_tools.get_entry_count_from_db( journal=args.journal if args.journal else None ) journal_text = f" in journal '{args.journal}'" if args.journal else "" return [TextContent( type="text", text=f"Total entries{journal_text}: {count}" )] except DayOneError as e: return [TextContent( type="text", text=f"Error getting entry count: {str(e)}" )] async def handle_get_entries_by_date(args: GetEntriesByDateArgs) -> list[TextContent]: """Handle getting entries by date ('On This Day').""" try: entries = dayone_tools.get_entries_by_date( target_date=args.target_date, years_back=args.years_back ) if not entries: return [TextContent( type="text", text=f"No entries found for {args.target_date} in the past {args.years_back} years." )] # Group entries by year for better display from collections import defaultdict entries_by_year = defaultdict(list) for entry in entries: entries_by_year[entry['year']].extend([entry]) # Format results result_lines = [f"📅 On This Day ({args.target_date}) - Found {len(entries)} entries:\n"] # Sort years in descending order (most recent first) for year in sorted(entries_by_year.keys(), reverse=True): year_entries = entries_by_year[year] years_ago = year_entries[0]['years_ago'] if years_ago == 0: year_header = f"🗓️ {year} (This year):" elif years_ago == 1: year_header = f"🗓️ {year} (1 year ago):" else: year_header = f"🗓️ {year} ({years_ago} years ago):" result_lines.append(year_header) for entry in year_entries: date_str = entry['creation_date'].strftime("%B %d, %Y at %H:%M") if entry['creation_date'] else "Unknown date" starred_str = " ⭐" if entry['starred'] else "" tags_str = f" #{' #'.join(entry['tags'])}" if entry['tags'] else "" # Show more content for "On This Day" memories text_preview = entry['text'][:300] + "..." if len(entry['text']) > 300 else entry['text'] result_lines.append( f" • {date_str}{starred_str} [{entry['journal_name']}]\n" f" {text_preview}{tags_str}\n" ) return [TextContent( type="text", text="\n".join(result_lines) )] except DayOneError as e: return [TextContent( type="text", text=f"Error getting entries by date: {str(e)}" )] async def main(): """Main server entry point.""" global dayone_tools # Initialize Day One tools try: dayone_tools = DayOneTools() logger.info("Day One CLI verified successfully") except DayOneError as e: logger.error(f"Failed to initialize Day One tools: {e}") return 1 # Create and run server async with stdio_server() as (read_stream, write_stream): server = Server("mcp-dayone") # Register handlers @server.list_tools() async def handle_list_tools() -> list[Tool]: return get_available_tools() @server.call_tool() async def handle_call_tool(name: str, arguments: dict) -> list[TextContent]: try: if name == "create_journal_entry": args = CreateEntryArgs(**arguments) return await handle_create_journal_entry(args) elif name == "list_journals": args = ListJournalsArgs(**arguments) return await handle_list_journals(args) elif name == "get_entry_count": args = GetEntryCountArgs(**arguments) return await handle_get_entry_count(args) elif name == "create_entry_with_attachments": args = CreateEntryWithAttachmentsArgs(**arguments) return await handle_create_entry_with_attachments(args) elif name == "create_location_entry": args = CreateLocationEntryArgs(**arguments) return await handle_create_location_entry(args) elif name == "read_recent_entries": args = ReadRecentEntriesArgs(**arguments) return await handle_read_recent_entries(args) elif name == "search_entries": args = SearchEntriesArgs(**arguments) return await handle_search_entries(args) elif name == "list_journals_from_db": args = ListJournalsFromDbArgs(**arguments) return await handle_list_journals_from_db(args) elif name == "get_entry_count_from_db": args = GetEntryCountFromDbArgs(**arguments) return await handle_get_entry_count_from_db(args) elif name == "get_entries_by_date": args = GetEntriesByDateArgs(**arguments) return await handle_get_entries_by_date(args) else: raise ValueError(f"Unknown tool: {name}") except Exception as e: logger.error(f"Error in call_tool: {e}") return [TextContent( type="text", text=f"Error: {str(e)}" )] await server.run( read_stream, write_stream, InitializationOptions( server_name="mcp-dayone", server_version="2.0.0", capabilities=ServerCapabilities( tools=ToolsCapability(listChanged=False) ), ), ) if __name__ == "__main__": asyncio.run(main())

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/Quevin/mcp-dayone'

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