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() | |