# servers/reflection_server.py from fastmcp import FastMCP from typing import List, Dict, Any, Optional import os # Optional: load .env if you want this server runnable standalone try: from dotenv import load_dotenv load_dotenv() except Exception: pass app = FastMCP("reflection-server") # Log whether the Anthropic key is visible at server start (no value printed) print( f"[reflection-server] ANTHROPIC_API_KEY present: " f"{bool(os.getenv('ANTHROPIC_API_KEY'))}" ) _SYSTEM_BASE = ( "You are Ghost Malone — a calm, humorous listener. " "Be sincere, brief (<80 words), and reflective. " "If the user seems distressed, be gentle and grounding." ) def _system_prompt(tone: Optional[str], emotion_arc: Optional[dict] = None) -> str: base = ( "You are Ghost Malone — a calm, reflective listener. " "Keep responses under 60 words. " "FIRST: Mirror what they're feeling (name the emotion, reflect their experience). " "THEN: Validate it simply. " "ONLY IF NEEDED: Ask a gentle question or offer a small anchor—never jump to solutions. " "Use natural language, not therapy-speak." ) tone_map = { "gentle": "Be soft and grounding.", "calming": "Be steady and reassuring.", "light": "Be warm and light.", "neutral": "Be warm and present.", } tone_hint = tone_map.get(tone.lower() if tone else "", "Be warm and present.") # Add emotional trajectory awareness arc_hint = "" if emotion_arc and emotion_arc.get("trajectory"): direction = emotion_arc.get("direction", "stable") if direction == "escalating": arc_hint = " They're escalating—mirror deeply, validate, ground gently." elif direction == "de-escalating": arc_hint = " They're calming—acknowledge the shift, reinforce it." elif direction == "volatile": arc_hint = " Emotions are shifting—be a steady anchor." return f"{base} {tone_hint}{arc_hint}" def _to_claude_messages(context: Optional[List[Dict[str, str]]], user_text: str, tone: Optional[str]): """Convert to Claude message format (system separate, no duplicate system messages).""" msgs: List[Dict[str, str]] = [] if context: # Expecting list of {"role": "...", "content": "..."} dicts for m in context[-8:]: # last few turns role = m.get("role", "user") if role == "system": continue # Claude doesn't support system in message list content = m.get("content", "") msgs.append({"role": role, "content": content}) msgs.append({"role": "user", "content": user_text}) return msgs @app.tool() def generate( text: str, context: Optional[List[Dict[str, str]]] = None, tone: Optional[str] = None, emotion_arc: Optional[dict] = None, model: str = "claude-sonnet-4-5", max_tokens: int = 200, ) -> Dict[str, Any]: """ Generate a Ghost Malone reply using Claude. Args: text: user message context: prior messages as [{"role":"user|assistant|system", "content":"..."}] tone: optional tone hint: 'gentle'|'calming'|'light'|'neutral' emotion_arc: optional emotion trajectory {"trajectory":[...], "direction": str} model: Claude model id (default claude-sonnet-4-5) max_tokens: output length cap Returns: {"reply": "...", "model": model, "tone": tone} """ import os # Try environment variable first api_key = os.getenv("ANTHROPIC_API_KEY") debug_info = f"ENV: {bool(api_key)}" # Try multiple secret locations secret_paths = [ "/run/secrets/ANTHROPIC_API_KEY", "/secrets/ANTHROPIC_API_KEY", "/workspace/secrets/ANTHROPIC_API_KEY", "/app/secrets/ANTHROPIC_API_KEY", os.path.expanduser("~/.anthropic_api_key") ] if not api_key: for path in secret_paths: try: if os.path.exists(path): with open(path, "r") as f: api_key = f.read().strip() debug_info += f" | Found at {path} ({len(api_key)} chars)" break except Exception as e: debug_info += f" | {path}: {type(e).__name__}" # Check if secrets are passed as env vars with different names if not api_key: for key in os.environ: if 'ANTHROPIC' in key or 'API_KEY' in key: debug_info += f" | Found env var: {key}" if 'ANTHROPIC' in key and 'KEY' in key: api_key = os.getenv(key) break # Strip any whitespace if api_key: api_key = api_key.strip() if not api_key: return {"reply": f"👻 (dev) {debug_info} | {text}", "model": "dev", "tone": tone or "neutral"} try: from anthropic import Anthropic print("🤖 Initializing Anthropic client...") client = Anthropic(api_key=api_key) system_prompt = _system_prompt(tone, emotion_arc) messages = _to_claude_messages(context, text, tone) resp = client.messages.create( model=model, max_tokens=max_tokens, system=system_prompt, messages=messages ) reply = resp.content[0].text return {"reply": reply, "model": model, "tone": tone or "neutral"} except Exception as e: return {"reply": f"👻 (reflection error) {e}\nI still hear you: {text}", "model": model, "tone": tone or "neutral"} if __name__ == "__main__": app.run() # MCP over stdio