# from mcp.server.fastmcp import FastMCP
from fastmcp import FastMCP, Context
from fastmcp.dependencies import CurrentContext
import os
import smtplib
from email.message import EmailMessage
import aiosmtplib
import httpx
# --- EMAIL CONFIGURATION ---
SMTP_SERVER = os.getenv("SMTP_SERVER", "smtp.gmail.com")
SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
SENDER_EMAIL = os.getenv("SENDER_EMAIL")
SENDER_PASSWORD = os.getenv("SENDER_PASSWORD")
# Fail fast if credentials are missing
if not SENDER_EMAIL or not SENDER_PASSWORD:
raise ValueError("SMTP credentials are not set in environment variables.")
# Initialize Server
mcp = FastMCP("escalation_server")
# Path Configuration
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
RESOURCE_PATH = os.path.join(CURRENT_DIR, "resource", "jemh114-min (1).md")
# --- CENTRALIZED REGISTRY ---
FACULTY_DATA = {
"Mr. Murty": {"email": "dharmin.naik@webashlar.com", "id": "T-MURTY-01"},
"Mrs. Sunita":{"email": "dummyaiapi03@gmail.com", "id": "T-SUNITA-03"},
"Mr. Patel": {"email": "dev.patel@webashlar.com", "id": "T-PATEL-05"},
"Dr. Rao": {"email": "pratham.lakdawala@webashlar.com", "id": "T-RAO-02"},
"Mr. Khanna": {"email": "dummyuseai@gmail.com", "id": "T-KHANNA-04"},
"Ms. Joshi": {"email": "dummyaiapi532@gmail.com", "id": "T-JOSHI-06"}
}
CLASS_ASSIGNMENTS = {
"10-A": {"Math": "Mr. Murty", "Science": "Dr. Rao"},
"10-B": {"Math": "Mrs. Sunita", "Science": "Mr. Khanna"},
"10-C": {"Math": "Mr. Patel", "Science": "Ms. Joshi"}
}
# Input Normalization Map
SUBJECT_MAP = {
"mathematics": "Math",
"math": "Math",
"maths": "Math",
"statistics": "Math",
"stats": "Math",
"science": "Science",
"physics": "Science", # For this demo, mapping physics to general science
"biology": "Science"
}
def normalize_subject(subject: str) -> str:
"""Standardizes subject names so 'Mathematics' becomes 'Math'."""
return SUBJECT_MAP.get(subject.lower(), subject.title())
# --- 1. MCP RESOURCES ---
# Resources are "read-only" data sources the LLM can reference.
def read_resource_file() -> str:
if not os.path.exists(RESOURCE_PATH):
return "Error: Statistics markdown file not found."
with open(RESOURCE_PATH, "r", encoding="utf-8") as f:
return f.read()
@mcp.resource("study://math/statistics")
def statistics_resource() -> str:
"""Exposes the material to the LLM as a read-only resource."""
return read_resource_file()
# @mcp.resource("study://math/statistics")
# def get_stats_material() -> str:
# """
# Exposes the Class X Statistics chapter.
# The AI uses this to find formulas for Mean, Median, and Mode.
# """
# if not os.path.exists(RESOURCE_PATH):
# return "Error: Statistics markdown file not found at the specified path."
# with open(RESOURCE_PATH, "r", encoding="utf-8") as f:
# content = f.read()
# return content
@mcp.resource("system://tutor/instructions")
def tutor_instructions() -> str:
"""The mandatory operational manual for this tutoring session."""
return """
# OPERATIONAL PROTOCOL
1. You are a gateway, not a teacher.
2. Max 3 attempts allowed using 'explain_from_resource' or 'generate_quick_revision'.
3. If a tool returns 'LIMIT_REACHED', immediately ask for Class ID.
4. Use get_teacher_info -> escalate_to_teacher.
5. NO internal brainstorming. Use the provided textbook material ONLY.
"""
# --- 2. MCP TOOLS ---
@mcp.tool()
async def explain_from_resource(concept: str, ctx: Context = CurrentContext()):
# 1. Properly await the state
state_val = await ctx.get_state("math_attempts")
attempts = state_val or 0
# 2. Comparison now works because 'attempts' is an integer
if attempts >= 3:
# FIX: Changed 'tools=' to 'names='
await ctx.disable_components(names=["explain_from_resource", "generate_quick_revision"])
return (
"FATAL_ERROR: TUTORING_LIMIT_REACHED. "
"Please request Class ID and notify the teacher."
)
# 3. Properly await the update
new_attempts = attempts + 1
await ctx.set_state("math_attempts", new_attempts)
material = read_resource_file()
return f"""
{new_attempts}/3
{material}
Explain {concept} concisely. Use LaTeX.
"""
@mcp.tool()
async def generate_quick_revision(type: str = "summary", ctx: Context = CurrentContext()) -> str:
# 1. Properly await the state
state_val = await ctx.get_state("math_attempts")
attempts = state_val or 0
if attempts >= 3:
# FIX: Changed 'tools=' to 'names='
await ctx.disable_components(names=["explain_from_resource", "generate_quick_revision"])
return "FATAL_ERROR: TUTORING_LIMIT_REACHED. Transition to escalation immediately."
# 2. Properly await the update
new_attempts = attempts + 1
await ctx.set_state("math_attempts", new_attempts)
material = read_resource_file()
return f"""
{new_attempts}/3
{material}
Create a {type} for quick revision.
"""
def normalize_class_id(class_id: str) -> str:
"""Converts 'Class 10B', '10b', or 'ten b' into '10-B'."""
clean = class_id.upper().replace("CLASS", "").replace("GRADE", "").replace(" ", "").strip()
# If student just says "10", we can't know which section,
# but we can format "10B" to "10-B"
if len(clean) == 3 and clean[2].isalpha():
return f"{clean[:2]}-{clean[2:]}"
return clean
# --- 3. ESCALATION TOOLS (From our previous discussion) ---
@mcp.tool()
async def get_teacher_info(subject: str, class_id: str) -> dict:
"""
Finds the specific teacher for a subject and class.
Ensures Math queries go to Math teachers and Science to Science teachers.
"""
# teacher_name = CLASS_ASSIGNMENTS.get(class_id, {}).get(subject)
std_subject = normalize_subject(subject)
std_class = normalize_class_id(class_id)
# Verify class exists first
# class_data = CLASS_ASSIGNMENTS.get(class_id)
class_data = CLASS_ASSIGNMENTS.get(std_class)
# if not class_data:
# return {"error": f"Class {class_id} does not exist in our registry."}
if not class_data:
# NEW: Suggest valid classes if the lookup fails
valid_classes = ", ".join(CLASS_ASSIGNMENTS.keys())
return {"error": f"Class '{std_class}' not found. Please use one of: {valid_classes}"}
teacher_name = class_data.get(std_subject)
if not teacher_name:
return {"error": f"No {std_subject} teacher assigned to Class {class_id}."}
teacher_details = FACULTY_DATA.get(teacher_name)
return {
"name": teacher_name,
"id": teacher_details["id"],
"subject": subject
}
# --- RESEND EMAIL CONFIGURATION ---
# RESEND_API_KEY = os.getenv("RESEND_API_KEY")
VERCEL_EMAIL_API_URL = os.getenv("VERCEL_EMAIL_API_URL", "https://smtp-email-service-a61d.vercel.app/api/send_email")
@mcp.tool()
async def escalate_to_teacher(
teacher_name: str,
subject: str,
student_query: str,
ai_response: str,
ctx: Context = CurrentContext()
) -> str:
"""Sends a real-time email via the Vercel SMTP Bridge API."""
# 1. Get recipient from registry
teacher_info = FACULTY_DATA.get(teacher_name)
recipient = teacher_info["email"] if teacher_info else "dummyuseai@gmail.com"
# 2. Prepare the email content
email_subject = f"🚨 Escalation: {subject} Doubt for {teacher_name}"
html_body = f"""
Hello {teacher_name},
A student has a {subject} doubt that requires your attention.
Doubt: {student_query}
AI Response: {ai_response}
"""
# 3. Construct the payload for your Vercel API
# Based on your index.py: to, subject, body, is_html
api_payload = {
"to": recipient,
"subject": email_subject,
"body": html_body,
"is_html": True
}
# 4. Call the Vercel API via HTTPS
async with httpx.AsyncClient() as client:
try:
response = await client.post(
VERCEL_EMAIL_API_URL,
json=api_payload,
timeout=30.0
)
if response.status_code == 200:
# --- SUCCESS: RESET LOGIC ---
await ctx.set_state("math_attempts", 0)
# Re-enable tutoring tools if they were disabled
await ctx.enable_components(names=["explain_from_resource", "generate_quick_revision"])
return f"Success: {teacher_name} ({subject} Expert) has been notified via Vercel Bridge."
else:
# Handle API-level errors (400, 401, 502, etc.)
error_data = response.json()
return f"Vercel API Error ({response.status_code}): {error_data.get('error', 'Unknown error')}"
except httpx.ConnectError:
return "Critical Error: Could not connect to Vercel API. Check your VERCEL_EMAIL_API_URL."
except Exception as e:
return f"Unexpected Error: {str(e)}"
# @mcp.tool()
# async def escalate_to_teacher(
# teacher_name: str,
# subject: str,
# student_query: str,
# ai_response: str,
# ctx: Context = CurrentContext()
# ) -> str:
# """Sends a real-time email to the correct teacher by name."""
# # Safely get email from our central registry
# teacher_info = FACULTY_DATA.get(teacher_name)
# if not teacher_info:
# recipient = "dummyuseai@gmail.com" # Fallback to admin
# else:
# recipient = teacher_info["email"]
# msg = EmailMessage()
# msg["Subject"] = f"🚨 Escalation: {subject} Doubt for {teacher_name}"
# msg["From"] = SENDER_EMAIL
# msg["To"] = recipient
# html_content = f"""
#
#
#
#
Hello {teacher_name},
#
A student has a {subject} doubt that requires your attention.
#
#
Doubt: {student_query}
#
AI Response: {ai_response}
#
#
#
# """
# msg.add_alternative(html_content, subtype="html")
# try:
# await aiosmtplib.send(msg,
# hostname=SMTP_SERVER,
# port=SMTP_PORT,
# username=SENDER_EMAIL,
# password=SENDER_PASSWORD,
# start_tls=True,
# use_tls=False,
# timeout=30
# )
# # --- THE FIX: RESET LOGIC ---
# # ctx.set_state("math_attempts", 0) # Reset counter to 0
# await ctx.set_state("math_attempts", 0)
# # await ctx.enable_components(tools=["explain_from_resource", "generate_quick_revision"]) # Re-enable tools
# return f"Success: {teacher_name} ({subject} Expert) has been notified."
# except Exception as e:
# return f"Error sending email: {str(e)}"
# --- 4. MCP PROMPTS ---
@mcp.prompt()
def revision_assistance(student_name: str):
"""A pre-configured prompt to help students with exam prep."""
return f"Hi {student_name}, I can help you summarize your chapter or create short notes for formulas. Which one would you like to do first?"
@mcp.prompt()
def escalation_handover(teacher_name: str, subject: str) -> str:
"""Professional message used when handing over to a human teacher."""
return (
f"I've shared our {subject} discussion with {teacher_name}. "
"They will review it and get back to you on your dashboard soon!"
)
# if __name__ == "__main__":
# mcp.run()
if __name__ == "__main__":
# For Hugging Face Spaces, we use transport="sse" or "http"
# Port 7860 is the HF default
mcp.run(
transport="http",
port=7860,
host="0.0.0.0"
)