Skip to main content
Glama

Customer Reminder MCP

by preshlele
reminder_app.py12.8 kB
# reminder_app.py import smtplib from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart import datetime import os from dotenv import load_dotenv import gspread from oauth2client.service_account import ServiceAccountCredentials from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore from apscheduler.executors.pool import ThreadPoolExecutor import atexit import sys import logging # Configure logging to file instead of stdout (for MCP compatibility) logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('reminder_app.log'), # Remove StreamHandler to avoid stdout pollution in MCP mode ] ) logger = logging.getLogger(__name__) class Reminder: """ Represents a single customer reminder with its context and actions. This acts as your 'Model Context Protocol' for reminder data. """ def __init__(self, customer_name, customer_email, message_template, due_date, reminder_type="email", customer_phone=None): self.customer_name = customer_name self.customer_email = customer_email self.message_template = message_template # The base message for the reminder self.due_date = due_date self.reminder_type = reminder_type self.customer_phone = customer_phone self.message_content = self._generate_message_content() def _generate_message_content(self): """ Generates the specific message content based on the template and customer data. This is where your 'contextual logic' comes in. You can customize this further (e.g., add personalization, product details). """ # Example: Replace placeholders in the template content = self.message_template.replace( "{customer_name}", self.customer_name) content = content.replace( "{due_date}", self.due_date.strftime("%Y-%m-%d %H:%M")) return content def get_formatted_email(self, sender_email, sender_name="Preshify"): """ Formats the reminder into an email message object. """ msg = MIMEMultipart() msg['From'] = f"{sender_name} <{sender_email}>" msg['To'] = self.customer_email msg['Subject'] = f"Reminder for {self.customer_name} - {self.due_date.strftime('%b %d')}" msg.attach(MIMEText(self.message_content, 'plain')) return msg class GoogleSheetsReader: """ Handles reading reminder data from Google Sheets. """ def __init__(self, credentials_file, spreadsheet_name): self.credentials_file = credentials_file self.spreadsheet_name = spreadsheet_name self.client = None self.sheet = None self._authenticate() def _authenticate(self): """Authenticate with Google Sheets API.""" try: scope = ['https://spreadsheets.google.com/feeds', 'https://www.googleapis.com/auth/drive'] creds = ServiceAccountCredentials.from_json_keyfile_name( self.credentials_file, scope) self.client = gspread.authorize(creds) self.sheet = self.client.open(self.spreadsheet_name).sheet1 logger.info( f"Successfully connected to Google Sheet: {self.spreadsheet_name}") except Exception as e: logger.error(f"Error connecting to Google Sheets: {e}") self.client = None self.sheet = None def get_reminders_data(self): """ Read reminder data from Google Sheets. Expected columns: customer_name, customer_email, message_template, due_date, reminder_type """ if not self.sheet: logger.warning( "Google Sheets not connected. Returning empty list.") return [] try: # Get all records as list of dictionaries records = self.sheet.get_all_records() reminders_data = [] today = datetime.datetime.now().date() for record in records: try: due_date_str = record.get('due_date', '') if not due_date_str: continue # Try different date formats for date_format in ['%Y-%m-%d', '%m/%d/%Y', '%d/%m/%Y', '%Y-%m-%d %H:%M', '%m/%d/%Y %H:%M', '%d/%m/%Y %H:%M']: try: due_date = datetime.datetime.strptime( due_date_str, date_format) break except ValueError: continue else: logger.warning(f"Could not parse date: {due_date_str}") continue # Check if reminder is due (today or overdue) if due_date.date() <= today: reminder_data = { 'customer_name': record.get('customer_name', ''), 'customer_email': record.get('customer_email', ''), 'message_template': record.get('message_template', 'Hi {customer_name}! This is a reminder for {due_date}.'), 'due_date': due_date, 'reminder_type': record.get('reminder_type', 'email') } # Only add if required fields are present if reminder_data['customer_name'] and reminder_data['customer_email']: reminders_data.append(reminder_data) except Exception as e: logger.error( f"Error processing record: {record}, Error: {e}") continue logger.info(f"Read {len(records)} records from the sheet") logger.info(f"Found {len(reminders_data)} reminders due for today") return reminders_data except Exception as e: logger.error(f"Error reading from Google Sheets: {e}") return [] class ReminderSender: """ Handles sending reminders via email. """ def __init__(self, smtp_server=None, smtp_port=None, smtp_username=None, smtp_password=None): self.smtp_server = smtp_server self.smtp_port = smtp_port self.smtp_username = smtp_username self.smtp_password = smtp_password def send_email_reminder(self, reminder: Reminder): """Sends an email reminder.""" if not self.smtp_server or not self.smtp_username or not self.smtp_password: logger.warning("Email sender not configured. Skipping email.") return try: msg = reminder.get_formatted_email(self.smtp_username) with smtplib.SMTP(self.smtp_server, self.smtp_port) as server: server.starttls() # Secure the connection server.login(self.smtp_username, self.smtp_password) server.sendmail(self.smtp_username, reminder.customer_email, msg.as_string()) logger.info( f"Email reminder sent to {reminder.customer_email} for {reminder.customer_name}") except Exception as e: logger.error( f"Error sending email to {reminder.customer_email}: {e}") def send_reminder(self, reminder: Reminder): """Dispatches the reminder (only email supported).""" if reminder.reminder_type == "email": self.send_email_reminder(reminder) else: logger.warning( f"Unsupported reminder type: {reminder.reminder_type}") # === STANDALONE FUNCTIONS (NO CLASS DEPENDENCIES) === # This prevents APScheduler serialization issues def load_sent_reminders(): """Load list of already sent reminders from file""" sent_file = "sent_reminders.txt" # todo: replace with SQL database sent_reminders = set() if os.path.exists(sent_file): try: with open(sent_file, 'r') as f: for line in f: sent_reminders.add(line.strip()) except Exception as e: logger.error(f"Error loading sent reminders: {e}") return sent_reminders def save_sent_reminder(reminder_id): """Save a reminder ID to the sent reminders file""" sent_file = "sent_reminders.txt" try: with open(sent_file, 'a') as f: f.write(f"{reminder_id}\n") except Exception as e: logger.error(f"Error saving sent reminder: {e}") def generate_reminder_id(customer_email, due_date): """Generate a unique ID for a reminder based on email and due date""" date_str = due_date.strftime("%Y-%m-%d") return f"{customer_email}_{date_str}" def check_and_send_reminders(): """Standalone function to run scheduled reminder checks - NO class dependencies""" logger.info( f"=== Reminder Check - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')} ===") try: load_dotenv() EMAIL_HOST = os.getenv("EMAIL_HOST") EMAIL_PORT = int(os.getenv("EMAIL_PORT", 587)) EMAIL_USERNAME = os.getenv("EMAIL_USERNAME") EMAIL_PASSWORD = os.getenv("EMAIL_PASSWORD") GOOGLE_CREDENTIALS_FILE = os.getenv( "GOOGLE_CREDENTIALS_FILE", "credentials.json") GOOGLE_SPREADSHEET_NAME = os.getenv( "GOOGLE_SPREADSHEET_NAME", "Customer_reminder") # Create fresh instances each time (no shared state) sender = ReminderSender( smtp_server=EMAIL_HOST, smtp_port=EMAIL_PORT, smtp_username=EMAIL_USERNAME, smtp_password=EMAIL_PASSWORD ) sheets_reader = GoogleSheetsReader( GOOGLE_CREDENTIALS_FILE, GOOGLE_SPREADSHEET_NAME) reminders_data = sheets_reader.get_reminders_data() if not reminders_data: logger.info("No reminders due at this time.") return # Load already sent reminders sent_reminders = load_sent_reminders() # Filter out already sent reminders new_reminders = [] for data in reminders_data: reminder_id = generate_reminder_id( data["customer_email"], data["due_date"]) if reminder_id not in sent_reminders: new_reminders.append(data) else: logger.info( f"Reminder already sent to {data['customer_email']} for {data['due_date'].strftime('%Y-%m-%d')} - skipping") if not new_reminders: logger.info("All due reminders have already been sent.") return # Process and send new reminders logger.info(f"Processing {len(new_reminders)} new reminders...") sent_count = 0 for data in new_reminders: reminder = Reminder( customer_name=data["customer_name"], customer_email=data["customer_email"], message_template=data["message_template"], due_date=data["due_date"], reminder_type=data["reminder_type"] ) sender.send_reminder(reminder) # Mark as sent reminder_id = generate_reminder_id( data["customer_email"], data["due_date"]) save_sent_reminder(reminder_id) sent_count += 1 logger.info(f"Successfully sent {sent_count} new reminders.") except Exception as e: logger.error(f"Error during reminder check: {e}") import traceback logger.error(traceback.format_exc()) def start_scheduler_service(interval_minutes=6): """Start the APScheduler service - standalone function""" # Configure job stores, executors and job defaults jobstores = { 'default': SQLAlchemyJobStore(url='sqlite:///reminder_jobs.sqlite') } executors = { 'default': ThreadPoolExecutor(10), } job_defaults = { 'coalesce': False, 'max_instances': 1 } scheduler = BackgroundScheduler( jobstores=jobstores, executors=executors, job_defaults=job_defaults ) # Schedule interval-based reminders (every X minutes) scheduler.add_job( func=check_and_send_reminders, # Use standalone function trigger="interval", minutes=interval_minutes, id='interval_reminders', replace_existing=True ) # Run immediately once on

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/preshlele/reminder-mcp'

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