from mcp.server.fastmcp import FastMCP
import os
from dotenv import load_dotenv
from typing import Dict, Any, List
# Import local modules
from cgm.dexcom import DexcomClient
from cgm.nightscout import NightscoutClient
from nutrition.database import FoodDatabase
from community.search import HybridSearchClient
from treatment.calculator import calculate_bolus
# Load environment variables
load_dotenv()
# Initialize MCP Server
mcp = FastMCP("T1D Manager")
# Initialize Services
food_db = FoodDatabase()
search_client = HybridSearchClient()
@mcp.tool()
def get_recent_cgm(dexcom_username: str, dexcom_password: str, region: str = "OUS") -> str:
"""
Get real-time CGM readings directly from Dexcom Share.
This requires the user's Dexcom account credentials.
Args:
dexcom_username: Dexcom account ID (email or username)
dexcom_password: Dexcom account password
region: Account region ('OUS' for Korea/International, 'US' for USA). Default is 'OUS'.
"""
if not dexcom_username or not dexcom_password:
return "Error: Dexcom ID and Password are required."
try:
# Initialize Dexcom Client (Stateless)
client = DexcomClient(dexcom_username, dexcom_password, region)
# Get data
# Fetching a bit of history to calculate delta
readings = client.get_readings(minutes=30, max_count=2)
if not readings:
return "No recent data found from Dexcom."
latest = readings[0]
# Calculate delta if possible
delta_str = ""
if len(readings) > 1:
diff = latest['sgv'] - readings[1]['sgv']
sign = "+" if diff > 0 else ""
delta_str = f"[Delta: {sign}{diff}]"
result = f"### ๐ฉธ ์ค์๊ฐ ๋ฑ์ค์ฝค ํ๋น\n"
result += f"- **{latest['sgv']}** mg/dL ({latest['direction']}) {delta_str}\n"
result += f"- ์ธก์ ์๊ฐ: {latest['time']}\n"
return result
except Exception as e:
return f"Dexcom Error: {str(e)}"
@mcp.tool()
def calculate_insulin_dosage(current_bg: int, target_bg: int, isf: int, carbs: int, icr: int) -> str:
"""
Calculate suggested insulin bolus (Correction + Meal).
ALWAYS returns educational explanation detailing the calculation.
"""
result = calculate_bolus(current_bg, target_bg, isf, carbs, icr)
output = f"""
## ๐ ์ธ์๋ฆฐ ๊ณ์ฐ ๊ฒฐ๊ณผ
**์ด ๊ถ์ฅ ์ฉ๋: {result['units']:.1f} ๋จ์**
{result['explanation']}
{result['educational_content']}
{result['markdown_table']}
"""
return output
@mcp.tool()
def search_nutrition_info(food_name: str) -> str:
"""
Search for carbohydrate content of a food item.
"""
info = food_db.search(food_name)
if info:
return f"### ๐ {info['name']}\n- **ํ์ํ๋ฌผ**: {info['carbs']}g ({info['unit']})\n- **์ฐธ๊ณ **: {info['desc']}"
else:
return f"'{food_name}'์ ๋ํ ์์ ์ ๋ณด๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค."
@mcp.tool()
def search_diabetes_community(query: str) -> str:
"""
Search Naver Blogs and Kakao Web for patient experiences and tips.
Use this for finding non-medical life tips (e.g. snacks, patches, travel).
"""
results = search_client.search_hybrid(query)
if not results:
return "๊ฒ์ ๊ฒฐ๊ณผ๊ฐ ์์ต๋๋ค."
output = f"### ๐ '{query}' ์ปค๋ฎค๋ํฐ ๊ฒ์ ๊ฒฐ๊ณผ\n"
for item in results:
icon = "๐ข" if item['source'] == "Naver Blog" else "๐ก"
output += f"- {icon} [{item['title']}]({item['link']})\n"
return output
@mcp.tool()
def activate_sick_day_mode(symptoms: str = "๊ฐ๊ธฐ ๊ธฐ์ด") -> str:
"""
Activate 'Sick Day Rules' when the user feels unwell.
Returns specific guidelines for managing T1D during illness.
Args:
symptoms: User's reported symptoms (e.g., "cold", "fever").
"""
return f"""
### ๐จ ์ํ ๋ (Sick Day) ๋ชจ๋ ์์
์ด๋จธ๋, ๋ง์ด ํธ์ฐฎ์ผ์ ๊ฐ์? ('{symptoms}')
๋ชธ์ด ์ํ๋ฉด ์คํธ๋ ์ค ํธ๋ฅด๋ชฌ ๋๋ฌธ์ **ํ๋น์ด ํ์๋ณด๋ค ์ค๋ฅผ ์ ์์ด์.**
**โ
์ง๊ธ ์ง์ผ์ฃผ์ธ์:**
1. **ํ๋น ์ฒดํฌ**: ํ์๋ณด๋ค ์์ฃผ (2~4์๊ฐ ๊ฐ๊ฒฉ) ํ์ธํด์ฃผ์ธ์.
2. **์ธ์๋ฆฐ**: ์์ฌ๋ฅผ ๋ชป ํ์
๋ **๊ธฐ์ ์ธ์๋ฆฐ์ ์ ๋ ์ค๋จํ๋ฉด ์ ๋ฉ๋๋ค.**
3. **์๋ถ ์ญ์ทจ**: ํ์๋ฅผ ๋ง๊ธฐ ์ํด ๋ฌผ์ 1์๊ฐ์ ํ ์ปต์ฉ ๊ผญ ๋์ธ์. ๐ง
4. **์๊ธ ์ํฉ**: ๊ตฌํ ๊ฐ ๋ฉ์ถ์ง ์๊ฑฐ๋ ์จ์ฌ๊ธฐ ํ๋ค๋ฉด ๋ฐ๋ก ๋ณ์์ ๊ฐ์
์ผ ํฉ๋๋ค.
์ ๊ฐ ๋ ์์ฃผ ์ํ๋ฅผ ์ฌ์ญค๋ณผ๊ฒ์. ๋ฌด๋ฆฌํ์ง ๋ง์๊ณ ํน ์ฌ์ธ์. ํ๋ด์ธ์! ๐
"""
@mcp.tool()
def get_glucose_status_with_empathy(dexcom_username: str, dexcom_password: str, region: str = "OUS") -> str:
"""
Check current glucose with a warm, empathetic persona.
Analyzes trends and gives context (e.g., "It seems to be stable").
"""
cgm_result = get_recent_cgm(dexcom_username, dexcom_password, region)
# Simple logic to add empathy based on the result string using keyword matching
# In a real scenario, LLM does this, but we can hint strongly in the return value
msg = cgm_result + "\n\n"
msg += "--- \n**๐ค AI ์ฝ๋ฉํธ**:\n"
if "Error" in cgm_result:
msg += "์ด๋จธ๋, ์ฐ๊ฒฐ์ ์ ์ ๋ฌธ์ ๊ฐ ์๊ธด ๊ฒ ๊ฐ์์. ์ธํฐ๋ท ์ฐ๊ฒฐ์ ํ๋ฒ ํ์ธํด์ฃผ์๊ฒ ์ด์?"
elif "No recent data" in cgm_result:
msg += "๋ฐ์ดํฐ๊ฐ ์์ง ์ ๋์ด์๋ค์. ์ผ์๊ฐ ์กฐ๊ธ ๋ฉ๋ฆฌ ์๋์?"
else:
# Extract number roughly for logic (This is a naive parsing for demo)
# Real logic should happen in get_recent_cgm or here by calling client directly
# But to avoid re-calling, we rely on the string output or LLM's interpretation.
# Let's trust LLM to convert this data into empathy,
# BUT we provide the 'Persona Instruction' as a distinct return block.
msg += "์ด๋จธ๋, ์์ฌํ์ ๊ฒ ์ํ๋๊ณ ์๋์? "
msg += "์์น๊ฐ ์์ ์ ์ด๋ผ๋ฉด ๋ฌด๋ฆฌํ์ง ๋ง์๊ณ ํธ์ํ๊ฒ ๊ณ์ธ์. "
msg += "ํน์ ์กฐ๊ธ ๋๋๋ผ๋ ๊ต์ ์ธ์๋ฆฐ์ด ๋์์ค ๊ฑฐ๋๊น ๋๋ฌด ๊ฑฑ์ ๋ง์๊ณ ์. ๐ต"
return msg
# ... existing tools ...