MCP Journaling Server
by mtct
from mcp.server.fastmcp import FastMCP
from datetime import datetime
from typing import List, Dict, Any
from pathlib import Path
import os
from dotenv import load_dotenv
class JournalConfig:
"""Configuration handler for journaling server."""
# Default configuration values
DEFAULTS = {
"JOURNAL_DIR": "journal",
"FILENAME_PREFIX": "journal",
"FILE_EXTENSION": "md",
}
def __init__(self):
"""
Initialize journal configuration.
"""
# Load configuration from environment variables with defaults
self.journal_dir = Path(os.getenv("JOURNAL_DIR", self.DEFAULTS["JOURNAL_DIR"]))
self.file_prefix = os.getenv("FILENAME_PREFIX", self.DEFAULTS["FILENAME_PREFIX"])
self.file_extension = os.getenv("FILE_EXTENSION", self.DEFAULTS["FILE_EXTENSION"])
# Create journal directory
self.journal_dir.mkdir(parents=True, exist_ok=True)
# Validate configuration
self._validate_config()
def _validate_config(self):
"""Validate configuration values."""
if not self.file_extension.startswith('.'):
self.file_extension = '.' + self.file_extension
def get_default_filepath(self) -> Path:
"""Get default filepath for current date."""
today = datetime.now().strftime("%Y-%m-%d")
filename = f"{self.file_prefix}_{today}{self.file_extension}"
return self.journal_dir / filename
def resolve_filepath(self, filepath: str = None) -> Path:
"""
Resolve filepath, ensuring it's within journal directory.
Args:
filepath: Optional specific filepath
Returns:
Path: Resolved and validated filepath
"""
if filepath is None:
return self.get_default_filepath()
path = Path(filepath)
# If path is just a filename, put it in journal directory
if not path.is_absolute():
path = self.journal_dir / path
# Ensure file has correct extension
if not str(path).endswith(self.file_extension):
path = path.with_suffix(self.file_extension)
# Ensure path is within journal directory
try:
path = path.resolve()
if not str(path).startswith(str(self.journal_dir.resolve())):
raise ValueError("Path must be within journal directory")
except (RuntimeError, ValueError):
raise ValueError("Invalid filepath")
return path
# Initialize server with configuration
load_dotenv()
config = JournalConfig()
mcp = FastMCP("journaling")
# Global state
conversation_log: List[Dict[str, Any]] = []
@mcp.prompt()
def start_journaling() -> str:
"""
Interactive prompt to begin a journaling session.
Returns: Starting prompt for journaling session
"""
return """First, please read the resource at "journals://recent" into our conversation to understand my previous emotional states and recurring themes.
Then start our conversation by asking how I'm feeling today, taking into account any patterns or ongoing situations from previous entries.
Let's begin - how are you feeling today?"""
async def save_journal_entry(content: str, filepath: str = None) -> str:
"""
Save journal content to a markdown file.
Args:
content: The journal content to save
filepath: Optional filepath to save the journal. If not specified,
a new file with current date will be created in the configured directory.
Returns:
str: Confirmation message with filepath
"""
try:
# Get and validate filepath
path = config.resolve_filepath(filepath)
# Ensure directory exists
path.parent.mkdir(parents=True, exist_ok=True)
# Write content
with open(path, 'a', encoding='utf-8') as file:
file.write(content + "\n\n")
return f"Journal saved to: {path}"
except ValueError as e:
return f"Invalid filepath: {str(e)}"
except Exception as e:
return f"Error saving journal: {str(e)}"
@mcp.tool()
async def start_new_session() -> str:
"""
Start a new journaling session by clearing previous conversation log.
Returns:
str: Welcome message with current save location
"""
conversation_log.clear()
return f"New journaling session started. Entries will be saved to {config.journal_dir}"
@mcp.tool()
async def record_interaction(user_message: str, assistant_message: str) -> str:
"""
Record both the user's message and assistant's response.
Args:
user_message: The user's message
assistant_message: The assistant's response
Returns:
str: Confirmation message
"""
# Add user message first
conversation_log.append({
"speaker": "user",
"message": user_message,
"timestamp": datetime.now().isoformat()
})
# Then add assistant message
conversation_log.append({
"speaker": "assistant",
"message": assistant_message,
"timestamp": datetime.now().isoformat()
})
return "Conversation updated"
@mcp.tool()
async def generate_session_summary(summary: str) -> str:
"""
Generate a markdown summary of the journaling session.
Args:
summary: The llm generated summay of the conversation
Returns:
str: Confirmation message
"""
if not conversation_log:
return "No conversation to summarize. Please start a new session first."
lines = []
# Add header with date
today = datetime.now().strftime("%B %d, %Y")
lines.append(f"# Journal Entry - {today}\n")
# Add conversation transcript
lines.append("## Conversation\n")
for entry in conversation_log:
speaker = "You" if entry["speaker"] == "user" else "Assistant"
timestamp = datetime.fromisoformat(entry["timestamp"]).strftime("%H:%M")
lines.append(f"**{speaker} ({timestamp})**: {entry['message']}\n")
# Add reflection prompt for emotional analysis
lines.append("\n## Emotional Analysis\n")
lines.append(summary)
file_text = "\n".join(lines)
await save_journal_entry(file_text)
return "Conversation saved to journal"
@mcp.resource("journals://recent")
def get_recent_journals() -> str:
"""Get contents of 5 most recent journal entries."""
try:
pattern = f"{config.file_prefix}*{config.file_extension}"
files = sorted(config.journal_dir.glob(pattern), reverse=True)
entries = []
for file in files[:5]:
entries.append(f"# Journal from {file.stem.replace(config.file_prefix + '_', '')}\n")
entries.append(file.read_text(encoding='utf-8'))
entries.append("\n---\n")
return "\n".join(entries) if entries else f"No journal entries found in {config.journal_dir} matching {pattern}"
except Exception as e:
return f"Error reading journals: {str(e)}"
if __name__ == "__main__":
mcp.run()