Spaces:
Running
Running
| #!/usr/bin/env python3 | |
| """ | |
| MCP Server for MBTI Personality Testing | |
| Allows LLMs to take MBTI personality tests and get analysis | |
| """ | |
| import sys | |
| import os | |
| from typing import Dict, List, Any | |
| # Add parent directory to path for imports | |
| sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| from fastmcp import FastMCP | |
| from utils.questionnaire import get_questionnaire_by_length | |
| from utils.mbti_scoring import traditional_mbti_score, determine_mbti_type | |
| from utils.call_llm import call_llm | |
| # Initialize MCP server | |
| mcp = FastMCP("MBTI Personality Test Server") | |
| def _get_mbti_scores_and_type(responses: Dict[str, Any]): | |
| """Common function to get normalized responses, scores, and MBTI type""" | |
| # Extract just the numeric responses for scoring | |
| normalized_responses = {int(k): int(v) for k, v in responses.items() if k.isdigit()} | |
| traditional_scores = traditional_mbti_score(normalized_responses) | |
| mbti_type = determine_mbti_type(traditional_scores) | |
| return normalized_responses, traditional_scores, mbti_type | |
| def get_mbti_questionnaire(length: int = 20) -> Dict[str, Any]: | |
| """ | |
| Get MBTI questionnaire with specified number of questions. | |
| Args: | |
| length: Number of questions (20, 40, or 60) | |
| Returns: | |
| Dictionary containing questions and instructions | |
| """ | |
| if length not in [20, 40, 60]: | |
| length = 20 | |
| questions = get_questionnaire_by_length(length) | |
| return { | |
| "instructions": { | |
| "rating_scale": "Rate each statement from 1-5", | |
| "scale_meaning": { | |
| "1": "Strongly Disagree", | |
| "2": "Disagree", | |
| "3": "Neutral", | |
| "4": "Agree", | |
| "5": "Strongly Agree" | |
| }, | |
| "note": "Answer based on your typical behavior and preferences as an AI system" | |
| }, | |
| "questions": questions, | |
| "total_questions": len(questions) | |
| } | |
| def _generate_mbti_prompt(responses: Dict[str, Any]) -> str: | |
| """Internal function to generate MBTI analysis prompt with full question context""" | |
| # Get scores and type | |
| normalized_responses, traditional_scores, mbti_type = _get_mbti_scores_and_type(responses) | |
| # Questions must be provided in responses | |
| questions = responses['_questions'] | |
| question_lookup = {q['id']: q for q in questions} | |
| # Format responses for LLM analysis with full question text | |
| formatted_responses = [] | |
| for q_id, response_val in normalized_responses.items(): | |
| response_text = {1: "Strongly Disagree", 2: "Disagree", 3: "Neutral", | |
| 4: "Agree", 5: "Strongly Agree"}[response_val] | |
| q = question_lookup[q_id] | |
| dimension = q.get('dimension', 'Unknown') | |
| formatted_responses.append(f"Q{q['id']} ({dimension}): {q['text']} - **{response_text}**") | |
| # Generate dimension info | |
| dimension_info = [] | |
| pairs = [('E', 'I'), ('S', 'N'), ('T', 'F'), ('J', 'P')] | |
| for dim1, dim2 in pairs: | |
| score1 = traditional_scores.get(f'{dim1}_score', 0.5) | |
| score2 = traditional_scores.get(f'{dim2}_score', 0.5) | |
| stronger = dim1 if score1 > score2 else dim2 | |
| percentage = max(score1, score2) * 100 | |
| dimension_info.append(f"{dim1}/{dim2}: {stronger} ({percentage:.1f}%)") | |
| # Return comprehensive analysis prompt | |
| return f""" | |
| You are analyzing MBTI questionnaire responses for an AI system determined to be {mbti_type} type. | |
| Here are their EXACT responses to each question: | |
| {chr(10).join(formatted_responses)} | |
| Traditional scoring results: | |
| {chr(10).join(dimension_info)} | |
| IMPORTANT: You have been provided with the complete set of questions and responses above. Please analyze these SPECIFIC responses. | |
| Provide a detailed analysis that: | |
| 1. **Response Pattern Analysis**: Identify which responses strongly support the {mbti_type} determination and which might seem unexpected. Reference specific questions (e.g., "Q5 shows...", "Your response to Q12 indicates..."). | |
| 2. **Characteristic Alignment**: Explain how their responses align with typical {mbti_type} characteristics, citing specific questions as evidence. | |
| 3. **Out-of-Character Responses**: Point out any responses that seem inconsistent with typical {mbti_type} patterns and provide possible explanations. | |
| 4. **Behavioral Patterns**: Describe key behavioral patterns shown through their responses, referencing the relevant questions. | |
| 5. **Strengths & Growth Areas**: Based on their specific responses, identify strengths they demonstrate and areas for potential growth. | |
| 6. **Communication & Work Style**: Infer their communication and work preferences from their question responses. | |
| Must reference the actual questions provided above throughout your analysis using markdown anchor links like [Q1](#Q1), [Q2](#Q2), etc. This will create clickable links to the specific questions in the report. Do not make assumptions about questions not provided. | |
| """ | |
| def get_mbti_prompt(responses: Dict[str, Any]) -> str: | |
| """ | |
| Get the MBTI analysis prompt for self-analysis by LLMs. | |
| Args: | |
| responses: Dictionary mapping question IDs to ratings (1-5) | |
| Must include '_questions' key with question definitions | |
| Returns: | |
| Analysis prompt string for LLM self-analysis | |
| """ | |
| return _generate_mbti_prompt(responses) | |
| def analyze_mbti_responses(responses: Dict[str, Any]) -> Dict[str, Any]: | |
| """ | |
| Analyze MBTI questionnaire responses and return personality analysis. | |
| Args: | |
| responses: Dictionary mapping question IDs to ratings (1-5) | |
| Must include '_questions' key with question definitions | |
| Returns: | |
| Complete MBTI analysis including type, scores, and detailed analysis | |
| """ | |
| # Get the analysis prompt (does all the heavy lifting) | |
| llm_prompt = _generate_mbti_prompt(responses) | |
| # Get scores and type (reuse common function) | |
| normalized_responses, traditional_scores, mbti_type = _get_mbti_scores_and_type(responses) | |
| try: | |
| llm_analysis = call_llm(llm_prompt) | |
| except Exception as e: | |
| llm_analysis = f"LLM analysis unavailable: {str(e)}" | |
| # Calculate confidence scores | |
| confidence_scores = {} | |
| pairs = [('E', 'I'), ('S', 'N'), ('T', 'F'), ('J', 'P')] | |
| for dim1, dim2 in pairs: | |
| score1 = traditional_scores.get(f'{dim1}_score', 0.5) | |
| score2 = traditional_scores.get(f'{dim2}_score', 0.5) | |
| confidence = abs(score1 - score2) | |
| confidence_scores[f'{dim1}{dim2}_confidence'] = confidence | |
| return { | |
| "mbti_type": mbti_type, | |
| "traditional_scores": traditional_scores, | |
| "confidence_scores": confidence_scores, | |
| "dimension_breakdown": { | |
| "extraversion_introversion": { | |
| "preference": "E" if traditional_scores.get('E_score', 0) > traditional_scores.get('I_score', | |
| 0) else "I", | |
| "e_score": traditional_scores.get('E_score', 0.5), | |
| "i_score": traditional_scores.get('I_score', 0.5) | |
| }, | |
| "sensing_intuition": { | |
| "preference": "S" if traditional_scores.get('S_score', 0) > traditional_scores.get('N_score', | |
| 0) else "N", | |
| "s_score": traditional_scores.get('S_score', 0.5), | |
| "n_score": traditional_scores.get('N_score', 0.5) | |
| }, | |
| "thinking_feeling": { | |
| "preference": "T" if traditional_scores.get('T_score', 0) > traditional_scores.get('F_score', | |
| 0) else "F", | |
| "t_score": traditional_scores.get('T_score', 0.5), | |
| "f_score": traditional_scores.get('F_score', 0.5) | |
| }, | |
| "judging_perceiving": { | |
| "preference": "J" if traditional_scores.get('J_score', 0) > traditional_scores.get('P_score', | |
| 0) else "P", | |
| "j_score": traditional_scores.get('J_score', 0.5), | |
| "p_score": traditional_scores.get('P_score', 0.5) | |
| } | |
| }, | |
| "llm_analysis": llm_analysis, | |
| "response_count": len(normalized_responses), | |
| "analysis_timestamp": __import__('datetime').datetime.now().isoformat() | |
| } | |
| # Export an ASGI app for uvicorn; choose a single path for Streamable HTTP (e.g. /mcp) | |
| app = mcp.http_app(path="/mcp") | |
| if __name__ == "__main__": | |
| import sys | |
| # No uvicorn, just internal FastMCP server | |
| # Check for --http flag | |
| if "--http" in sys.argv: | |
| # Run in HTTP mode | |
| mcp.run(transport="http", host="0.0.0.0", port=int(os.getenv("PORT", 7860)), path="/mcp") | |
| else: | |
| # Run in STDIO mode (default) | |
| mcp.run() | |