# Directory Structure ``` ├── .gitattributes ├── .gitignore ├── agents │ ├── agent_integration.py │ ├── agent_server.py │ ├── Dockerfile │ └── requirements.txt ├── client │ ├── css │ │ └── styles.css │ ├── index.html │ └── js │ ├── animations.js │ └── main.js ├── DEVELOPER.md ├── docker-compose.yml ├── Dockerfile ├── PROJECT_SUMMARY.md ├── QUICK_START.md ├── README.md ├── server │ ├── app.py │ ├── config.py │ ├── forms_api.py │ ├── mcp_handler.py │ ├── requirements.txt │ ├── static │ │ ├── animations.js │ │ ├── main.js │ │ └── styles.css │ ├── templates │ │ └── index.html │ └── utils │ ├── __init__.py │ └── logger.py ├── setup_credentials.sh ├── start_with_creds.sh ├── start.sh └── use_provided_creds.sh ``` # Files -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- ``` 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Environment variables 2 | .env 3 | .env.* 4 | 5 | # Python 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | *.so 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # Virtual Environment 29 | venv/ 30 | env/ 31 | ENV/ 32 | 33 | # Docker 34 | .dockerignore 35 | 36 | # IDE files 37 | .idea/ 38 | .vscode/ 39 | *.swp 40 | *.swo 41 | .DS_Store 42 | 43 | # Logs 44 | logs/ 45 | *.log 46 | 47 | # Testing 48 | .coverage 49 | htmlcov/ 50 | .pytest_cache/ 51 | .tox/ 52 | .nox/ 53 | coverage.html 54 | coverage.xml 55 | *.cover 56 | 57 | # Google API credentials 58 | credentials.json 59 | token.json ``` -------------------------------------------------------------------------------- /client/js/animations.js: -------------------------------------------------------------------------------- ```javascript 1 | ``` -------------------------------------------------------------------------------- /server/utils/__init__.py: -------------------------------------------------------------------------------- ```python 1 | """Utility functions for the Google Forms MCP Server.""" ``` -------------------------------------------------------------------------------- /agents/requirements.txt: -------------------------------------------------------------------------------- ``` 1 | requests==2.28.2 2 | flask==2.2.3 3 | flask-cors==3.0.10 4 | werkzeug==2.2.3 5 | numpy==1.24.2 6 | websockets==11.0.2 7 | pymongo==4.3.3 8 | camel-ai 9 | Pillow 10 | # Add Google Generative AI SDK 11 | google-generativeai 12 | ``` -------------------------------------------------------------------------------- /server/requirements.txt: -------------------------------------------------------------------------------- ``` 1 | flask==2.2.3 2 | flask-cors==3.0.10 3 | werkzeug==2.2.3 4 | google-auth==2.17.3 5 | google-auth-oauthlib==1.0.0 6 | google-auth-httplib2==0.1.0 7 | google-api-python-client==2.86.0 8 | python-dotenv==1.0.0 9 | requests==2.28.2 10 | gunicorn==20.1.0 11 | websockets==11.0.2 12 | uuid==1.30 13 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | FROM python:3.9-slim 2 | 3 | WORKDIR /app 4 | 5 | # Copy requirements first to leverage Docker caching 6 | COPY server/requirements.txt . 7 | 8 | # Install dependencies 9 | RUN pip install --no-cache-dir -r requirements.txt 10 | 11 | # Copy server code 12 | COPY server /app/server 13 | COPY .env /app/.env 14 | 15 | # Set working directory to server 16 | WORKDIR /app/server 17 | 18 | # Set environment variables 19 | ENV PYTHONUNBUFFERED=1 20 | 21 | # Expose port 22 | EXPOSE 5000 23 | 24 | # Run the server 25 | CMD ["python", "app.py"] 26 | ``` -------------------------------------------------------------------------------- /agents/Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | FROM python:3.9-slim 2 | 3 | # Install curl for healthcheck 4 | RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* 5 | 6 | WORKDIR /app 7 | 8 | # Copy requirements first to leverage Docker caching 9 | COPY agents/requirements.txt . 10 | 11 | # Install dependencies 12 | RUN pip install --no-cache-dir -r requirements.txt 13 | 14 | # Copy agent code 15 | COPY agents /app/agents 16 | 17 | # Set working directory to agents 18 | WORKDIR /app/agents 19 | 20 | # Set environment variables 21 | ENV PYTHONUNBUFFERED=1 22 | 23 | # Expose port 24 | EXPOSE 5001 25 | 26 | # Use --app to specify the application file 27 | CMD ["flask", "--app", "agent_server:app", "run", "--host=0.0.0.0", "--port=5001"] ``` -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- ```yaml 1 | version: '3.8' 2 | 3 | services: 4 | # MCP Server Service 5 | mcp-server: 6 | build: 7 | context: . 8 | dockerfile: Dockerfile 9 | container_name: google-form-mcp-server 10 | ports: 11 | - "5005:5000" 12 | volumes: 13 | - ./server:/app/server 14 | env_file: 15 | - .env 16 | environment: 17 | - FLASK_ENV=development 18 | - DEBUG=True 19 | - AGENT_ENDPOINT=http://agents:5001/process 20 | depends_on: 21 | agents: 22 | condition: service_healthy 23 | restart: unless-stopped 24 | networks: 25 | - mcp-network 26 | 27 | # CamelAIOrg Agents Service 28 | agents: 29 | build: 30 | context: . 31 | dockerfile: agents/Dockerfile 32 | container_name: camelai-agents 33 | ports: 34 | - "5006:5001" 35 | volumes: 36 | - ./agents:/app/agents 37 | environment: 38 | - MCP_SERVER_URL=http://mcp-server:5000/api/process 39 | - PORT=5001 40 | - GOOGLE_API_KEY={{GOOGLE_API_KEY}} 41 | healthcheck: 42 | test: ["CMD", "curl", "--fail", "http://localhost:5001/health"] 43 | interval: 10s 44 | timeout: 5s 45 | retries: 5 46 | start_period: 30s 47 | restart: unless-stopped 48 | networks: 49 | - mcp-network 50 | 51 | networks: 52 | mcp-network: 53 | driver: bridge 54 | ``` -------------------------------------------------------------------------------- /use_provided_creds.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Setup the project with user-provided credentials 4 | 5 | echo "================================================" 6 | echo " Google Forms MCP Server - Use Provided Credentials" 7 | echo "================================================" 8 | echo "" 9 | 10 | # Check if credentials were provided 11 | if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then 12 | echo "Usage: $0 <client_id> <client_secret> <refresh_token>" 13 | echo "" 14 | echo "Example:" 15 | echo "$0 123456789-abcdef.apps.googleusercontent.com GOCSPX-abc123def456 1//04abcdefghijklmnop" 16 | exit 1 17 | fi 18 | 19 | # Get credentials from arguments 20 | CLIENT_ID=$1 21 | CLIENT_SECRET=$2 22 | REFRESH_TOKEN=$3 23 | 24 | # Create .env file with provided credentials 25 | cat > .env << EOF 26 | # Google API Credentials 27 | GOOGLE_CLIENT_ID=$CLIENT_ID 28 | GOOGLE_CLIENT_SECRET=$CLIENT_SECRET 29 | GOOGLE_REFRESH_TOKEN=$REFRESH_TOKEN 30 | 31 | # Server Configuration 32 | FLASK_ENV=development 33 | PORT=5000 34 | DEBUG=True 35 | 36 | # CamelAIOrg Agents Configuration 37 | AGENT_ENDPOINT=http://agents:5001/process 38 | AGENT_API_KEY=demo_key 39 | EOF 40 | 41 | echo "Created .env file with your provided credentials." 42 | echo "" 43 | echo "Starting the project now..." 44 | echo "" 45 | 46 | # Start the project 47 | ./start.sh ``` -------------------------------------------------------------------------------- /server/config.py: -------------------------------------------------------------------------------- ```python 1 | import os 2 | from dotenv import load_dotenv 3 | 4 | # Load environment variables from .env file 5 | load_dotenv() 6 | 7 | # Google API settings 8 | GOOGLE_CLIENT_ID = os.getenv('GOOGLE_CLIENT_ID') 9 | GOOGLE_CLIENT_SECRET = os.getenv('GOOGLE_CLIENT_SECRET') 10 | GOOGLE_REFRESH_TOKEN = os.getenv('GOOGLE_REFRESH_TOKEN') 11 | 12 | # API scopes needed for Google Forms 13 | # Include more scopes to ensure proper access 14 | SCOPES = [ 15 | 'https://www.googleapis.com/auth/forms', 16 | 'https://www.googleapis.com/auth/forms.body', 17 | 'https://www.googleapis.com/auth/drive', 18 | 'https://www.googleapis.com/auth/drive.file', 19 | 'https://www.googleapis.com/auth/drive.metadata' 20 | ] 21 | 22 | # Server settings 23 | PORT = int(os.getenv('PORT', 5000)) 24 | DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' 25 | 26 | # CamelAIOrg Agent settings 27 | AGENT_ENDPOINT = os.getenv('AGENT_ENDPOINT', 'http://agents:5001/process') 28 | AGENT_API_KEY = os.getenv('AGENT_API_KEY') 29 | 30 | # MCP Protocol settings 31 | MCP_VERSION = "1.0.0" 32 | MCP_TOOLS = [ 33 | "create_form", 34 | "add_question", 35 | "get_responses" 36 | ] 37 | 38 | # LLM Settings / Gemini Settings (Update this section) 39 | # REMOVE LLM_API_KEY and LLM_API_ENDPOINT 40 | GEMINI_API_KEY = os.getenv('GEMINI_API_KEY') 41 | # The specific model endpoint will be constructed in the agent 42 | ``` -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Display welcome message 4 | echo "================================================" 5 | echo " Google Forms MCP Server with CamelAIOrg Agents" 6 | echo "================================================" 7 | echo "" 8 | 9 | # Check if .env file exists 10 | if [ ! -f ".env" ]; then 11 | echo "Error: .env file not found!" 12 | echo "Please create a .env file with your Google API credentials." 13 | echo "Example format:" 14 | echo "GOOGLE_CLIENT_ID=your_client_id" 15 | echo "GOOGLE_CLIENT_SECRET=your_client_secret" 16 | echo "GOOGLE_REFRESH_TOKEN=your_refresh_token" 17 | echo "" 18 | echo "PORT=5000" 19 | echo "DEBUG=True" 20 | echo "" 21 | exit 1 22 | fi 23 | 24 | # Build and start containers 25 | echo "Starting services with Docker Compose..." 26 | docker-compose up --build -d 27 | 28 | # Wait for services to start 29 | echo "Waiting for services to start..." 30 | sleep 5 31 | 32 | # Display service status 33 | echo "" 34 | echo "Service Status:" 35 | docker-compose ps 36 | 37 | # Display access information 38 | echo "" 39 | echo "Access Information:" 40 | echo "- MCP Server: http://localhost:5005" 41 | echo "- Agent Server: http://localhost:5006" 42 | echo "- Client Interface: Open client/index.html in your browser" 43 | echo "" 44 | echo "To view logs:" 45 | echo " docker-compose logs -f" 46 | echo "" 47 | echo "To stop the services:" 48 | echo " docker-compose down" 49 | echo "" 50 | echo "Enjoy using the Google Forms MCP Server!" ``` -------------------------------------------------------------------------------- /start_with_creds.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Setup the project with provided credentials 4 | 5 | echo "================================================" 6 | echo " Google Forms MCP Server - Quick Start" 7 | echo "================================================" 8 | echo "" 9 | echo "This script will set up the project with provided credentials" 10 | echo "and start the services." 11 | echo "" 12 | 13 | # Create .env file with provided credentials 14 | cat > .env << EOF 15 | # Google API Credentials 16 | GOOGLE_CLIENT_ID=your_client_id_here 17 | GOOGLE_CLIENT_SECRET=your_client_secret_here 18 | GOOGLE_REFRESH_TOKEN=your_refresh_token_here 19 | 20 | # Server Configuration 21 | FLASK_ENV=development 22 | PORT=5000 23 | DEBUG=True 24 | 25 | # CamelAIOrg Agents Configuration 26 | AGENT_ENDPOINT=http://agents:5001/process 27 | AGENT_API_KEY=demo_key 28 | EOF 29 | 30 | echo "Created .env file with placeholder credentials." 31 | echo "" 32 | echo "IMPORTANT: You need to edit the .env file with your actual Google API credentials." 33 | echo "You can do this now by running:" 34 | echo " nano .env" 35 | echo "" 36 | echo "After updating your credentials, start the project with:" 37 | echo " ./start.sh" 38 | echo "" 39 | echo "Would you like to edit the .env file now? (y/n)" 40 | read edit_now 41 | 42 | if [ "$edit_now" == "y" ]; then 43 | ${EDITOR:-nano} .env 44 | 45 | echo "" 46 | echo "Now that you've updated your credentials, would you like to start the project? (y/n)" 47 | read start_now 48 | 49 | if [ "$start_now" == "y" ]; then 50 | ./start.sh 51 | else 52 | echo "You can start the project later by running ./start.sh" 53 | fi 54 | else 55 | echo "Remember to update your credentials in .env before starting the project." 56 | fi ``` -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 | <title>Google Forms MCP Client</title> 7 | <link rel="stylesheet" href="css/styles.css"> 8 | </head> 9 | <body> 10 | <div class="container"> 11 | <h1>Google Forms MCP Client</h1> 12 | <p>This client connects to the Google Forms MCP Server and CamelAIOrg Agents to create forms using natural language.</p> 13 | 14 | <div class="server-status"> 15 | <h2>Server Status</h2> 16 | <p>MCP Server: <span id="serverStatus">Unknown</span></p> 17 | </div> 18 | 19 | <a href="http://localhost:5005" class="button pulse" id="connectButton">Connect to Server</a> 20 | 21 | <div class="status loading" id="loadingMessage"> 22 | Connecting to server... 23 | </div> 24 | 25 | <div class="status error" id="errorMessage"> 26 | Could not connect to the server. Please make sure the MCP server is running. 27 | </div> 28 | 29 | <div class="features"> 30 | <h2>Features</h2> 31 | <ul> 32 | <li>Create Google Forms with natural language</li> 33 | <li>Multiple question types support</li> 34 | <li>View form creation process in real-time</li> 35 | <li>Monitor API requests and responses</li> 36 | </ul> 37 | </div> 38 | 39 | <div class="footer"> 40 | <p>Google Forms MCP Server with CamelAIOrg Agents Integration</p> 41 | </div> 42 | </div> 43 | 44 | <script src="js/main.js"></script> 45 | </body> 46 | </html> 47 | ``` -------------------------------------------------------------------------------- /setup_credentials.sh: -------------------------------------------------------------------------------- ```bash 1 | #!/bin/bash 2 | 3 | # Google Forms MCP Server Credentials Setup Script 4 | 5 | echo "================================================" 6 | echo " Google Forms MCP Server - Credentials Setup" 7 | echo "================================================" 8 | echo "" 9 | echo "This script will help you set up your Google API credentials." 10 | echo "You'll need to provide your Client ID, Client Secret, and Refresh Token." 11 | echo "" 12 | echo "If you don't have these credentials yet, please follow the instructions in README.md" 13 | echo "" 14 | 15 | # Check if .env already exists 16 | if [ -f ".env" ]; then 17 | read -p ".env file already exists. Do you want to overwrite it? (y/n): " overwrite 18 | if [ "$overwrite" != "y" ]; then 19 | echo "Setup cancelled." 20 | exit 0 21 | fi 22 | fi 23 | 24 | # Get credentials 25 | read -p "Enter your Google Client ID: " client_id 26 | read -p "Enter your Google Client Secret: " client_secret 27 | read -p "Enter your Google Refresh Token: " refresh_token 28 | 29 | # Get port settings 30 | read -p "Enter port for MCP Server [5000]: " port 31 | port=${port:-5000} 32 | 33 | # Get debug setting 34 | read -p "Enable debug mode? (y/n) [y]: " debug_mode 35 | debug_mode=${debug_mode:-y} 36 | 37 | if [ "$debug_mode" == "y" ]; then 38 | debug="True" 39 | else 40 | debug="False" 41 | fi 42 | 43 | # Create .env file 44 | cat > .env << EOF 45 | # Google API Credentials 46 | GOOGLE_CLIENT_ID=$client_id 47 | GOOGLE_CLIENT_SECRET=$client_secret 48 | GOOGLE_REFRESH_TOKEN=$refresh_token 49 | 50 | # Server Configuration 51 | FLASK_ENV=development 52 | PORT=$port 53 | DEBUG=$debug 54 | 55 | # CamelAIOrg Agents Configuration 56 | AGENT_ENDPOINT=http://agents:5001/process 57 | AGENT_API_KEY=demo_key 58 | EOF 59 | 60 | echo "" 61 | echo "Credentials saved to .env file." 62 | echo "" 63 | echo "You can now start the project with:" 64 | echo "./start.sh" 65 | echo "" 66 | echo "If you need to update your credentials later, you can run this script again" 67 | echo "or edit the .env file directly." ``` -------------------------------------------------------------------------------- /server/utils/logger.py: -------------------------------------------------------------------------------- ```python 1 | import logging 2 | import sys 3 | import json 4 | from datetime import datetime 5 | 6 | # Configure logger 7 | logger = logging.getLogger("mcp_server") 8 | logger.setLevel(logging.INFO) 9 | 10 | # Console handler 11 | console_handler = logging.StreamHandler(sys.stdout) 12 | console_handler.setLevel(logging.INFO) 13 | 14 | # Format 15 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 16 | console_handler.setFormatter(formatter) 17 | 18 | # Add handler to logger 19 | logger.addHandler(console_handler) 20 | 21 | 22 | def log_mcp_request(request_data): 23 | """Log an incoming MCP request.""" 24 | try: 25 | transaction_id = request_data.get('transaction_id', 'unknown') 26 | tool_name = request_data.get('tool_name', 'unknown') 27 | 28 | logger.info(f"MCP Request [{transaction_id}] - Tool: {tool_name}") 29 | logger.debug(f"Request data: {json.dumps(request_data, indent=2)}") 30 | 31 | except Exception as e: 32 | logger.error(f"Error logging MCP request: {str(e)}") 33 | 34 | 35 | def log_mcp_response(response_data): 36 | """Log an outgoing MCP response.""" 37 | try: 38 | transaction_id = response_data.get('transaction_id', 'unknown') 39 | status = response_data.get('status', 'unknown') 40 | 41 | logger.info(f"MCP Response [{transaction_id}] - Status: {status}") 42 | logger.debug(f"Response data: {json.dumps(response_data, indent=2)}") 43 | 44 | except Exception as e: 45 | logger.error(f"Error logging MCP response: {str(e)}") 46 | 47 | 48 | def log_error(message, error=None): 49 | """Log an error.""" 50 | try: 51 | if error: 52 | logger.error(f"{message}: {str(error)}") 53 | else: 54 | logger.error(message) 55 | except Exception as e: 56 | # Last resort if logging itself fails 57 | print(f"Logging error: {str(e)}") 58 | 59 | 60 | def get_logger(): 61 | """Get the configured logger.""" 62 | return logger 63 | ``` -------------------------------------------------------------------------------- /client/css/styles.css: -------------------------------------------------------------------------------- ```css 1 | /* Google Forms MCP Client Styles */ 2 | :root { 3 | --bg-color: #121212; 4 | --panel-bg: #1e1e1e; 5 | --panel-border: #333333; 6 | --text-color: #e0e0e0; 7 | --highlight-color: #00ccff; 8 | --secondary-highlight: #00ff9d; 9 | --danger-color: #ff4757; 10 | --warning-color: #ffa502; 11 | --success-color: #2ed573; 12 | --muted-color: #747d8c; 13 | } 14 | 15 | body { 16 | font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; 17 | background-color: var(--bg-color); 18 | color: var(--text-color); 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | height: 100vh; 24 | margin: 0; 25 | padding: 20px; 26 | text-align: center; 27 | } 28 | 29 | .container { 30 | max-width: 800px; 31 | padding: 30px; 32 | background-color: var(--panel-bg); 33 | border-radius: 8px; 34 | border: 1px solid var(--panel-border); 35 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 36 | } 37 | 38 | h1 { 39 | color: var(--highlight-color); 40 | margin-bottom: 10px; 41 | } 42 | 43 | h2 { 44 | color: var(--secondary-highlight); 45 | font-size: 1.4rem; 46 | margin: 20px 0 15px 0; 47 | } 48 | 49 | p { 50 | color: var(--muted-color); 51 | margin-bottom: 30px; 52 | line-height: 1.5; 53 | } 54 | 55 | .button { 56 | background-color: var(--highlight-color); 57 | color: #000; 58 | font-weight: 500; 59 | padding: 12px 24px; 60 | border: none; 61 | border-radius: 4px; 62 | text-decoration: none; 63 | cursor: pointer; 64 | transition: background-color 0.3s; 65 | display: inline-block; 66 | } 67 | 68 | .button:hover { 69 | background-color: #00a3cc; 70 | } 71 | 72 | .button-secondary { 73 | background-color: transparent; 74 | border: 1px solid var(--secondary-highlight); 75 | color: var(--secondary-highlight); 76 | } 77 | 78 | .button-secondary:hover { 79 | background-color: rgba(0, 255, 157, 0.1); 80 | } 81 | 82 | .status { 83 | margin-top: 20px; 84 | padding: 15px; 85 | border-radius: 4px; 86 | display: none; 87 | } 88 | 89 | .status.loading { 90 | background-color: rgba(255, 165, 2, 0.1); 91 | color: var(--warning-color); 92 | border: 1px solid var(--warning-color); 93 | } 94 | 95 | .status.success { 96 | background-color: rgba(46, 213, 115, 0.1); 97 | color: var(--success-color); 98 | border: 1px solid var(--success-color); 99 | } 100 | 101 | .status.error { 102 | background-color: rgba(255, 71, 87, 0.1); 103 | color: var(--danger-color); 104 | border: 1px solid var(--danger-color); 105 | } 106 | 107 | .footer { 108 | margin-top: 50px; 109 | color: var(--muted-color); 110 | font-size: 0.8rem; 111 | } 112 | 113 | /* Animation */ 114 | @keyframes pulse { 115 | 0% { 116 | box-shadow: 0 0 0 0 rgba(0, 204, 255, 0.7); 117 | } 118 | 70% { 119 | box-shadow: 0 0 0 10px rgba(0, 204, 255, 0); 120 | } 121 | 100% { 122 | box-shadow: 0 0 0 0 rgba(0, 204, 255, 0); 123 | } 124 | } 125 | 126 | .pulse { 127 | animation: pulse 1.5s infinite; 128 | } 129 | ``` -------------------------------------------------------------------------------- /client/js/main.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Google Forms MCP Client 3 | * Main JavaScript file for handling server connection and UI updates 4 | */ 5 | 6 | document.addEventListener('DOMContentLoaded', function() { 7 | // DOM Elements 8 | const connectButton = document.getElementById('connectButton'); 9 | const loadingMessage = document.getElementById('loadingMessage'); 10 | const errorMessage = document.getElementById('errorMessage'); 11 | const serverStatusElement = document.getElementById('serverStatus'); 12 | 13 | // Server configuration 14 | const config = { 15 | mcpServerUrl: 'http://localhost:5005', 16 | agentServerUrl: 'http://localhost:5006' 17 | }; 18 | 19 | // Initialize 20 | function init() { 21 | // Add event listeners 22 | if (connectButton) { 23 | connectButton.addEventListener('click', handleServerConnect); 24 | } 25 | 26 | // Check server status 27 | checkServerStatus(); 28 | } 29 | 30 | // Check if MCP server is available 31 | async function checkServerStatus() { 32 | if (serverStatusElement) { 33 | serverStatusElement.textContent = 'Checking...'; 34 | serverStatusElement.className = 'checking'; 35 | } 36 | 37 | try { 38 | const response = await fetch(`${config.mcpServerUrl}/api/health`); 39 | 40 | if (response.ok) { 41 | const data = await response.json(); 42 | if (serverStatusElement) { 43 | serverStatusElement.textContent = `Online (v${data.version})`; 44 | serverStatusElement.className = 'online'; 45 | } 46 | return true; 47 | } else { 48 | if (serverStatusElement) { 49 | serverStatusElement.textContent = 'Error'; 50 | serverStatusElement.className = 'offline'; 51 | } 52 | return false; 53 | } 54 | } catch (error) { 55 | console.error('Server status check error:', error); 56 | if (serverStatusElement) { 57 | serverStatusElement.textContent = 'Offline'; 58 | serverStatusElement.className = 'offline'; 59 | } 60 | return false; 61 | } 62 | } 63 | 64 | // Handle server connection button click 65 | function handleServerConnect(e) { 66 | if (e) { 67 | e.preventDefault(); 68 | } 69 | 70 | if (loadingMessage) { 71 | loadingMessage.style.display = 'block'; 72 | } 73 | 74 | if (errorMessage) { 75 | errorMessage.style.display = 'none'; 76 | } 77 | 78 | // Attempt to connect to the server 79 | fetch(`${config.mcpServerUrl}/api/health`) 80 | .then(response => { 81 | if (response.ok) { 82 | window.location.href = config.mcpServerUrl; 83 | } else { 84 | throw new Error('Server error'); 85 | } 86 | }) 87 | .catch(error => { 88 | console.error('Connection error:', error); 89 | if (loadingMessage) { 90 | loadingMessage.style.display = 'none'; 91 | } 92 | 93 | if (errorMessage) { 94 | errorMessage.style.display = 'block'; 95 | } 96 | }); 97 | } 98 | 99 | // Start the application 100 | init(); 101 | }); 102 | ``` -------------------------------------------------------------------------------- /PROJECT_SUMMARY.md: -------------------------------------------------------------------------------- ```markdown 1 | # Google Form MCP Server with CamelAIOrg Agents - Project Summary 2 | 3 | ## Project Overview 4 | 5 | This project provides a complete implementation of an **MCP (Model Context Protocol) Server** integrated with the **Google Forms API** and **CamelAIOrg Agents**. The system enables the creation of Google Forms through natural language instructions, with a visually appealing UI that displays the request flow. 6 | 7 | ## Key Components 8 | 9 | ### 1. MCP Server (Python/Flask) 10 | - Implements the Model Context Protocol 11 | - Exposes tools for Google Forms operations 12 | - Handles authentication with Google APIs 13 | - Processes structured API requests/responses 14 | 15 | ### 2. CamelAIOrg Agents (Python/Flask) 16 | - Processes natural language requests 17 | - Extracts intent and parameters 18 | - Converts natural language to structured MCP calls 19 | - Handles form creation logic 20 | 21 | ### 3. Frontend UI (HTML/CSS/JavaScript) 22 | - Dark-themed modern interface 23 | - Real-time flow visualization with animations 24 | - MCP packet logging and display 25 | - Form result presentation 26 | 27 | ### 4. Dockerized Deployment 28 | - Docker and Docker Compose configuration 29 | - Separate containers for server and agents 30 | - Environment configuration 31 | - Easy one-command deployment 32 | 33 | ## Feature Highlights 34 | 35 | ### Natural Language Form Creation 36 | Users can create forms with simple instructions like: 37 | - "Create a customer feedback form with rating questions" 38 | - "Make a survey about remote work preferences" 39 | - "Set up an RSVP form for my event" 40 | 41 | ### Question Type Support 42 | The system supports multiple question types: 43 | - Text (short answer) 44 | - Paragraph (long answer) 45 | - Multiple-choice 46 | - Checkbox 47 | 48 | ### Visual Flow Representation 49 | The UI visualizes the flow of requests and responses: 50 | - Frontend → Agent → MCP Server → Google Forms API 51 | - Animated particles showing data movement 52 | - Active node highlighting 53 | - Error visualization 54 | 55 | ### MCP Protocol Implementation 56 | Full implementation of the Model Context Protocol: 57 | - Structured tool definitions 58 | - Transaction-based processing 59 | - Schema validation 60 | - Error handling 61 | 62 | ### Security Considerations 63 | - OAuth2 authentication with Google APIs 64 | - Environment-based configuration 65 | - Credential management 66 | - Input validation 67 | 68 | ## Technical Achievements 69 | 70 | 1. **Modular Architecture**: Clean separation between MCP server, agent logic, and UI 71 | 2. **Interactive Visualization**: Real-time animation of request/response flows 72 | 3. **Agent Intelligence**: Natural language processing for form creation 73 | 4. **Protocol Implementation**: Complete MCP protocol implementation 74 | 5. **Containerized Deployment**: Docker-based deployment for easy setup 75 | 76 | ## User Experience 77 | 78 | The system provides a seamless user experience: 79 | 1. User enters a natural language request 80 | 2. Request is visualized flowing through the system components 81 | 3. Form is created with appropriate questions 82 | 4. User receives a link to view/edit the form 83 | 5. Questions and responses can be managed 84 | 85 | ## Future Enhancements 86 | 87 | Potential areas for future development: 88 | 1. Advanced NLP for more complex form requests 89 | 2. Additional question types and form features 90 | 3. Integration with other Google Workspace products 91 | 4. Form templates and preset configurations 92 | 5. User authentication and form management 93 | 94 | ## Conclusion 95 | 96 | This project demonstrates the power of combining AI agents with structured protocols (MCP) to enable natural language interfaces for productivity tools. The implementation showcases modern web development practices, API integration, and containerized deployment, all while providing an intuitive and visually appealing user interface. ``` -------------------------------------------------------------------------------- /QUICK_START.md: -------------------------------------------------------------------------------- ```markdown 1 | # Quick Start Guide 2 | 3 | Follow these steps to get the Google Forms MCP Server up and running quickly. 4 | 5 | ## Prerequisites 6 | 7 | - Docker and Docker Compose installed 8 | - Google account with access to Google Forms 9 | - Google Cloud Platform project with Forms API enabled 10 | 11 | ## Setup in 5 Steps 12 | 13 | ### 1. Clone the Repository 14 | 15 | ```bash 16 | git clone https://github.com/yourusername/google-form-mcp-server.git 17 | cd google-form-mcp-server 18 | ``` 19 | 20 | ### 2. Get Google API Credentials 21 | 22 | If you already have your credentials, proceed to step 3. Otherwise: 23 | 24 | 1. Go to [Google Cloud Console](https://console.cloud.google.com/) 25 | 2. Create a project and enable Google Forms API and Google Drive API 26 | 3. Create OAuth 2.0 credentials (Web application type) 27 | 4. Use the OAuth 2.0 Playground to get a refresh token: 28 | - Go to: https://developers.google.com/oauthplayground/ 29 | - Configure with your credentials (⚙️ icon) 30 | - Select Forms and Drive API scopes 31 | - Authorize and exchange for tokens 32 | 33 | ### 3. Run the Credentials Setup Script 34 | 35 | ```bash 36 | ./setup_credentials.sh 37 | ``` 38 | 39 | Enter your Google API credentials when prompted. 40 | 41 | ### 4. Start the Services 42 | 43 | ```bash 44 | ./start.sh 45 | ``` 46 | 47 | This will build and start the Docker containers for the MCP Server and CamelAIOrg Agents. 48 | 49 | ### 5. Access the Application 50 | 51 | Open your browser and navigate to: 52 | 53 | - Server UI: http://localhost:5005 54 | - Manual client: Open `client/index.html` in your browser 55 | 56 | ## Using the Application 57 | 58 | 1. Enter natural language instructions like: 59 | - "Create a customer feedback form with a rating question" 60 | - "Create a survey about remote work preferences" 61 | - "Make an RSVP form for my event" 62 | 63 | 2. Watch the request flow through the system visualization 64 | 65 | 3. View the generated form details and links 66 | 67 | ## Troubleshooting 68 | 69 | - **Server not starting**: Check Docker is running and ports 5005/5006 are available 70 | - **Authentication errors**: Verify your credentials in the .env file 71 | - **Connection errors**: Ensure your network allows API calls to Google 72 | 73 | ## What's Next? 74 | 75 | - Read the full README.md for detailed information 76 | - Check DEVELOPER.md for technical documentation 77 | - Explore the codebase to understand the implementation 78 | 79 | <!DOCTYPE html> 80 | <html lang="en"> 81 | <head> 82 | <meta charset="UTF-8"> 83 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 84 | <title>Google Forms MCP Client</title> 85 | <link rel="stylesheet" href="css/styles.css"> 86 | </head> 87 | <body> 88 | <div class="container"> 89 | <h1>Google Forms MCP Client</h1> 90 | <p>This client connects to the Google Forms MCP Server and CamelAIOrg Agents to create forms using natural language.</p> 91 | 92 | <div class="server-status"> 93 | <h2>Server Status</h2> 94 | <p>MCP Server: <span id="serverStatus">Unknown</span></p> 95 | </div> 96 | 97 | <a href="http://localhost:5005" class="button pulse" id="connectButton">Connect to Server</a> 98 | 99 | <div class="status loading" id="loadingMessage"> 100 | Connecting to server... 101 | </div> 102 | 103 | <div class="status error" id="errorMessage"> 104 | Could not connect to the server. Please make sure the MCP server is running. 105 | </div> 106 | 107 | <div class="features"> 108 | <h2>Features</h2> 109 | <ul> 110 | <li>Create Google Forms with natural language</li> 111 | <li>Multiple question types support</li> 112 | <li>View form creation process in real-time</li> 113 | <li>Monitor API requests and responses</li> 114 | </ul> 115 | </div> 116 | 117 | <div class="footer"> 118 | <p>Google Forms MCP Server with CamelAIOrg Agents Integration</p> 119 | </div> 120 | </div> 121 | 122 | <script src="js/main.js"></script> 123 | </body> 124 | </html> ``` -------------------------------------------------------------------------------- /agents/agent_server.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | Agent Server for CamelAIOrg Integration with Google Forms MCP 3 | 4 | This module provides a Flask-based REST API that accepts natural language requests, 5 | processes them through the FormAgent, and returns the results. 6 | """ 7 | 8 | from flask import Flask, request, jsonify 9 | from flask_cors import CORS 10 | import json 11 | import os 12 | import logging 13 | 14 | from agent_integration import FormAgent 15 | 16 | # Configure logging 17 | logging.basicConfig( 18 | level=logging.INFO, 19 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 20 | ) 21 | logger = logging.getLogger("agent_server") 22 | 23 | # Create Flask app 24 | app = Flask(__name__) 25 | CORS(app) # Enable CORS for all routes 26 | 27 | # Initialize form agent 28 | form_agent = FormAgent() 29 | 30 | # Configuration 31 | PORT = int(os.getenv('PORT', 5001)) 32 | DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' 33 | 34 | @app.route('/health', methods=['GET']) 35 | def health_check(): 36 | """Health check endpoint.""" 37 | return jsonify({ 38 | "status": "ok", 39 | "service": "CamelAIOrg Agent Server", 40 | "version": "1.0.0" 41 | }) 42 | 43 | @app.route('/process', methods=['POST']) 44 | def process_request(): 45 | """ 46 | Process a natural language request by passing it to the FormAgent. 47 | The FormAgent is responsible for NLP and interacting with the MCP server. 48 | 49 | Request format: 50 | { 51 | "request_text": "Create a feedback form with 3 questions" 52 | } 53 | 54 | Response format (on success): 55 | { 56 | "status": "success", 57 | "result": { ... form details ... } 58 | } 59 | Response format (on error): 60 | { 61 | "status": "error", 62 | "message": "Error description" 63 | } 64 | """ 65 | try: 66 | if not request.is_json: 67 | return jsonify({ 68 | "status": "error", 69 | "message": "Request must be JSON" 70 | }), 400 71 | 72 | data = request.get_json() 73 | 74 | if 'request_text' not in data: 75 | return jsonify({ 76 | "status": "error", 77 | "message": "Missing required parameter 'request_text'" 78 | }), 400 79 | 80 | request_text = data['request_text'] 81 | logger.info(f"Agent server received request: {request_text}") 82 | 83 | # Process the request through the FormAgent 84 | # The FormAgent's process_request method will handle NLP, 85 | # MCP packet creation, and communication with the MCP server. 86 | result = form_agent.process_request(request_text) 87 | 88 | # The agent's response (success or error) is returned directly 89 | return jsonify(result) 90 | 91 | except Exception as e: 92 | logger.error(f"Error processing agent request: {str(e)}", exc_info=True) 93 | return jsonify({ 94 | "status": "error", 95 | "message": f"Internal Agent Server Error: {str(e)}" 96 | }), 500 97 | 98 | @app.route('/schema', methods=['GET']) 99 | def get_schema(): 100 | """Return the agent capabilities schema.""" 101 | schema = { 102 | "name": "Google Forms Creator Agent", 103 | "description": "Creates Google Forms from natural language requests", 104 | "capabilities": [ 105 | { 106 | "name": "create_form", 107 | "description": "Create a new Google Form with questions" 108 | }, 109 | { 110 | "name": "add_question", 111 | "description": "Add a question to an existing form" 112 | }, 113 | { 114 | "name": "get_responses", 115 | "description": "Get responses from an existing form" 116 | } 117 | ], 118 | "example_requests": [ 119 | "Create a customer feedback form with rating questions", 120 | "Make a survey about remote work preferences", 121 | "Set up an RSVP form for my event" 122 | ] 123 | } 124 | 125 | return jsonify({ 126 | "status": "success", 127 | "schema": schema 128 | }) 129 | 130 | # Run the Flask app 131 | if __name__ == '__main__': 132 | logger.info(f"Starting CamelAIOrg Agent Server on port {PORT}") 133 | logger.info(f"Debug mode: {DEBUG}") 134 | 135 | app.run(host='0.0.0.0', port=PORT, debug=DEBUG) ``` -------------------------------------------------------------------------------- /server/app.py: -------------------------------------------------------------------------------- ```python 1 | from flask import Flask, request, jsonify, render_template 2 | from flask_cors import CORS 3 | import json 4 | import os 5 | import requests # Import requests library 6 | 7 | from mcp_handler import MCPHandler 8 | from utils.logger import log_mcp_request, log_mcp_response, log_error, get_logger 9 | import config 10 | from forms_api import GoogleFormsAPI 11 | 12 | # Initialize Flask application 13 | app = Flask(__name__) 14 | CORS(app) # Enable CORS for all routes 15 | 16 | # Initialize the MCP handler 17 | mcp_handler = MCPHandler() 18 | logger = get_logger() 19 | forms_api = GoogleFormsAPI() 20 | 21 | @app.route('/') 22 | def index(): 23 | """Render the main page of the application.""" 24 | return render_template('index.html') 25 | 26 | @app.route('/api/schema', methods=['GET']) 27 | def get_schema(): 28 | """Return the MCP tools schema.""" 29 | try: 30 | schema = mcp_handler.get_tools_schema() 31 | return jsonify({ 32 | "status": "success", 33 | "tools": schema, 34 | "version": config.MCP_VERSION 35 | }) 36 | except Exception as e: 37 | log_error("Error returning schema", e) 38 | return jsonify({ 39 | "status": "error", 40 | "message": str(e) 41 | }), 500 42 | 43 | @app.route('/api/process', methods=['POST']) 44 | def process_mcp_request(): 45 | """Process an MCP request.""" 46 | try: 47 | if not request.is_json: 48 | return jsonify({ 49 | "status": "error", 50 | "message": "Request must be JSON" 51 | }), 400 52 | 53 | request_data = request.get_json() 54 | log_mcp_request(request_data) 55 | 56 | response = mcp_handler.process_request(request_data) 57 | log_mcp_response(response) 58 | 59 | return jsonify(response) 60 | 61 | except Exception as e: 62 | log_error("Error processing MCP request", e) 63 | return jsonify({ 64 | "status": "error", 65 | "message": str(e) 66 | }), 500 67 | 68 | @app.route('/api/health', methods=['GET']) 69 | def health_check(): 70 | """Health check endpoint.""" 71 | return jsonify({ 72 | "status": "ok", 73 | "version": config.MCP_VERSION 74 | }) 75 | 76 | # WebSocket for real-time UI updates 77 | @app.route('/ws') 78 | def websocket(): 79 | """WebSocket endpoint for real-time updates.""" 80 | return render_template('websocket.html') 81 | 82 | @app.route('/api/forms', methods=['POST']) 83 | def forms_api(): 84 | """Handle form operations.""" 85 | try: 86 | data = request.json 87 | action = data.get('action') 88 | 89 | logger.info(f"Received form API request: {action}") 90 | 91 | if action == 'create_form': 92 | title = data.get('title', 'Untitled Form') 93 | description = data.get('description', '') 94 | 95 | logger.info(f"Creating form with title: {title}") 96 | result = forms_api.create_form(title, description) 97 | logger.info(f"Form created with ID: {result.get('form_id')}") 98 | logger.info(f"Form response URL: {result.get('response_url')}") 99 | logger.info(f"Form edit URL: {result.get('edit_url')}") 100 | 101 | return jsonify({"status": "success", "result": result}) 102 | 103 | return jsonify({"status": "error", "message": f"Unknown action: {action}"}) 104 | except Exception as e: 105 | logger.error(f"Error in forms API: {str(e)}") 106 | return jsonify({"status": "error", "message": str(e)}), 500 107 | 108 | @app.route('/api/agent_proxy', methods=['POST']) 109 | def agent_proxy(): 110 | """Proxy requests from the frontend to the agent server.""" 111 | try: 112 | frontend_data = request.get_json() 113 | if not frontend_data or 'request_text' not in frontend_data: 114 | log_error("Agent proxy received invalid data from frontend", frontend_data) 115 | return jsonify({"status": "error", "message": "Invalid request data"}), 400 116 | 117 | agent_url = config.AGENT_ENDPOINT 118 | if not agent_url: 119 | log_error("Agent endpoint URL is not configured.", None) 120 | return jsonify({"status": "error", "message": "Agent service URL not configured."}), 500 121 | 122 | logger.info(f"Proxying request to agent at {agent_url}: {frontend_data}") 123 | 124 | # Forward the request to the agent server 125 | # Note: Consider adding timeout and error handling for the requests.post call 126 | agent_response = requests.post( 127 | agent_url, 128 | json=frontend_data, 129 | headers={'Content-Type': 'application/json'} 130 | # Potentially add API key header if agent requires it: 131 | # headers={"Authorization": f"Bearer {config.AGENT_API_KEY}"} 132 | ) 133 | 134 | # Check agent response status 135 | agent_response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx) 136 | 137 | agent_data = agent_response.json() 138 | logger.info(f"Received response from agent: {agent_data}") 139 | 140 | # Return the agent's response directly to the frontend 141 | return jsonify(agent_data) 142 | 143 | except requests.exceptions.RequestException as e: 144 | log_error(f"Error communicating with agent server at {config.AGENT_ENDPOINT}", e) 145 | return jsonify({"status": "error", "message": f"Failed to reach agent service: {str(e)}"}), 502 # Bad Gateway 146 | except Exception as e: 147 | log_error("Error in agent proxy endpoint", e) 148 | return jsonify({"status": "error", "message": f"Internal proxy error: {str(e)}"}), 500 149 | 150 | if __name__ == '__main__': 151 | port = config.PORT 152 | debug = config.DEBUG 153 | 154 | logger.info(f"Starting Google Forms MCP Server on port {port}") 155 | logger.info(f"Debug mode: {debug}") 156 | 157 | app.run(host='0.0.0.0', port=port, debug=debug) 158 | ``` -------------------------------------------------------------------------------- /server/templates/index.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 | <title>Google Forms MCP Server</title> 7 | <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"> 8 | <link rel="stylesheet" href="{{ url_for('static', filename='styles.css') }}"> 9 | </head> 10 | <body> 11 | <div class="container-fluid"> 12 | <header class="header"> 13 | <div class="logo-container"> 14 | <h1>Google Forms MCP Server</h1> 15 | <p class="subtitle">Integrated with CamelAIOrg Agents</p> 16 | </div> 17 | <div class="status-indicator"> 18 | <span id="statusDot" class="status-dot"></span> 19 | <span id="statusText">Connecting...</span> 20 | </div> 21 | </header> 22 | 23 | <div class="row main-content"> 24 | <!-- Left Panel: Form Controls --> 25 | <div class="col-md-4"> 26 | <div class="panel"> 27 | <h2>Form Request</h2> 28 | <div class="form-group"> 29 | <label for="requestInput">Natural Language Request:</label> 30 | <textarea id="requestInput" class="form-control" rows="4" placeholder="Create a feedback form with 3 questions about customer service..."></textarea> 31 | </div> 32 | <button id="sendRequestBtn" class="btn btn-primary">Process Request</button> 33 | <div class="demo-actions mt-4"> 34 | <h3>Quick Demo Actions</h3> 35 | <button class="btn btn-outline-secondary demo-btn" data-request="Create a customer feedback form with a rating question from 1-5 and a text question for additional comments">Feedback Form</button> 36 | <button class="btn btn-outline-secondary demo-btn" data-request="Create a survey about remote work preferences with 3 multiple choice questions">Work Survey</button> 37 | <button class="btn btn-outline-secondary demo-btn" data-request="Create an event RSVP form with name, email and attendance options">RSVP Form</button> 38 | </div> 39 | </div> 40 | </div> 41 | 42 | <!-- Middle Panel: Flow Visualization --> 43 | <div class="col-md-4"> 44 | <div class="panel flow-panel"> 45 | <h2>Request Flow</h2> 46 | <div class="stage-indicator"> 47 | Current: <span id="stageIndicator">Ready for request</span> 48 | </div> 49 | <div class="flow-container"> 50 | <div class="node" id="frontendNode"> 51 | <div class="node-icon"> 52 | <i class="node-glow"></i> 53 | </div> 54 | <span class="node-label">Frontend</span> 55 | </div> 56 | <div class="flow-line" id="frontendToAgent"> 57 | <div class="flow-particle-container"></div> 58 | </div> 59 | <div class="node" id="agentNode"> 60 | <div class="node-icon"> 61 | <i class="node-glow"></i> 62 | </div> 63 | <span class="node-label">CamelAIOrg Agent</span> 64 | </div> 65 | <div class="flow-line" id="agentToMCP"> 66 | <div class="flow-particle-container"></div> 67 | </div> 68 | <div class="node" id="mcpNode"> 69 | <div class="node-icon"> 70 | <i class="node-glow"></i> 71 | </div> 72 | <span class="node-label">MCP Server</span> 73 | </div> 74 | <div class="flow-line" id="mcpToGoogle"> 75 | <div class="flow-particle-container"></div> 76 | </div> 77 | <div class="node" id="googleNode"> 78 | <div class="node-icon"> 79 | <i class="node-glow"></i> 80 | </div> 81 | <span class="node-label">Google Forms</span> 82 | </div> 83 | </div> 84 | </div> 85 | </div> 86 | 87 | <!-- Right Panel: MCP Packet & Results --> 88 | <div class="col-md-4"> 89 | <div class="panel result-panel"> 90 | <h2>Results</h2> 91 | <div id="formResult" class="hidden"> 92 | <h3 id="formTitle">Form Title</h3> 93 | <div class="form-links"> 94 | <a id="formViewLink" href="#" target="_blank" class="btn btn-sm btn-outline-info">View Form</a> 95 | <a id="formEditLink" href="#" target="_blank" class="btn btn-sm btn-outline-warning">Edit Form</a> 96 | </div> 97 | <div id="formQuestions" class="mt-3"> 98 | <h4>Questions:</h4> 99 | <ul id="questionsList" class="list-group"> 100 | <!-- Questions will be added here --> 101 | </ul> 102 | </div> 103 | </div> 104 | <div id="mcp-log"> 105 | <h3>MCP Packet Log</h3> 106 | <div id="packetLog" class="log-container"> 107 | <!-- Packet logs will be added here --> 108 | </div> 109 | </div> 110 | </div> 111 | </div> 112 | </div> 113 | </div> 114 | 115 | <!-- Templates for dynamic content --> 116 | <template id="packetTemplate"> 117 | <div class="packet-entry"> 118 | <div class="packet-header"> 119 | <span class="transaction-id"></span> 120 | <span class="packet-time"></span> 121 | <span class="packet-direction"></span> 122 | </div> 123 | <pre class="packet-content"></pre> 124 | </div> 125 | </template> 126 | 127 | <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> 128 | <script src="{{ url_for('static', filename='animations.js') }}"></script> 129 | <script src="{{ url_for('static', filename='main.js') }}"></script> 130 | </body> 131 | </html> 132 | ``` -------------------------------------------------------------------------------- /DEVELOPER.md: -------------------------------------------------------------------------------- ```markdown 1 | # Google Forms MCP Server - Developer Guide 2 | 3 | This document provides technical details for developers who want to understand, modify, or extend the Google Forms MCP Server and CamelAIOrg Agents integration. 4 | 5 | ## Architecture Overview 6 | 7 | The system is built on a modular architecture with the following key components: 8 | 9 | 1. **MCP Server (Flask)**: Implements the Model Context Protocol and interfaces with Google Forms API 10 | 2. **CamelAIOrg Agents (Flask)**: Processes natural language and converts it to structured MCP calls 11 | 3. **Frontend UI (HTML/CSS/JS)**: Visualizes the flow and manages user interactions 12 | 4. **Google Forms API**: External service for form creation and management 13 | 14 | ### Component Communication 15 | 16 | ``` 17 | Frontend → Agents → MCP Server → Google Forms API 18 | ``` 19 | 20 | Each component communicates via HTTP requests, with MCP packets being the primary data format for the MCP Server. 21 | 22 | ## MCP Protocol Specification 23 | 24 | The MCP (Model Context Protocol) is designed to standardize tool usage between AI agents and external services. Our implementation follows these guidelines: 25 | 26 | ### Request Format 27 | 28 | ```json 29 | { 30 | "transaction_id": "unique_id", 31 | "tool_name": "tool_name", 32 | "parameters": { 33 | "param1": "value1", 34 | "param2": "value2" 35 | } 36 | } 37 | ``` 38 | 39 | ### Response Format 40 | 41 | ```json 42 | { 43 | "transaction_id": "unique_id", 44 | "status": "success|error", 45 | "result": { 46 | "key1": "value1", 47 | "key2": "value2" 48 | }, 49 | "error": { 50 | "message": "Error message" 51 | } 52 | } 53 | ``` 54 | 55 | ## Google Forms API Integration 56 | 57 | The integration with Google Forms API is handled by the `GoogleFormsAPI` class in `forms_api.py`. Key methods include: 58 | 59 | - `create_form(title, description)`: Creates a new form 60 | - `add_question(form_id, question_type, title, options, required)`: Adds a question 61 | - `get_responses(form_id)`: Retrieves form responses 62 | 63 | ### Authentication Flow 64 | 65 | 1. OAuth2 credentials are stored in environment variables 66 | 2. Credentials are loaded and used to create a Google API client 67 | 3. API requests are authenticated using these credentials 68 | 69 | ## CamelAIOrg Agent Implementation 70 | 71 | The agent implementation in `agent_integration.py` provides the natural language processing interface. Key methods: 72 | 73 | - `process_request(request_text)`: Main entry point for NL requests 74 | - `_analyze_request(request_text)`: Analyzes and extracts intent from text 75 | - `_handle_create_form(params)`: Creates a form based on extracted parameters 76 | 77 | ## Frontend Visualization 78 | 79 | The UI uses a combination of CSS and JavaScript to visualize the request flow: 80 | 81 | - **Flow Lines**: Represent the path between components 82 | - **Particles**: Animated elements that travel along flow lines 83 | - **Nodes**: Represent each component (Frontend, Agents, MCP, Google) 84 | - **MCP Packet Log**: Shows the actual MCP packets being exchanged 85 | 86 | ### Animation System 87 | 88 | The animation system in `animations.js` provides these key features: 89 | 90 | - `animateFlow(fromNode, toNode, direction)`: Animates flow between nodes 91 | - `animateRequestResponseFlow()`: Animates a complete request-response cycle 92 | - `animateErrorFlow(errorStage)`: Visualizes errors at different stages 93 | 94 | ## Extending the System 95 | 96 | ### Adding New Question Types 97 | 98 | To add a new question type: 99 | 100 | 1. Update the `add_question` method in `forms_api.py` 101 | 2. Add the new type to the validation in `_handle_add_question` in `mcp_handler.py` 102 | 3. Update the UI logic in `main.js` to handle the new question type 103 | 104 | ### Adding New MCP Tools 105 | 106 | To add a new MCP tool: 107 | 108 | 1. Add the tool name to `MCP_TOOLS` in `config.py` 109 | 2. Add tool schema to `get_tools_schema` in `mcp_handler.py` 110 | 3. Create a handler method `_handle_tool_name` in `mcp_handler.py` 111 | 4. Implement the underlying functionality in `forms_api.py` 112 | 113 | ### Enhancing Agent Capabilities 114 | 115 | To improve the agent's language processing: 116 | 117 | 1. Enhance the `_analyze_request` method in `agent_integration.py` 118 | 2. Add new intents and parameters recognition 119 | 3. Adjust the question generation logic based on request analysis 120 | 121 | ## Testing 122 | 123 | ### Unit Testing 124 | 125 | Test individual components: 126 | 127 | ```bash 128 | # Test MCP handler 129 | python -m unittest tests/test_mcp_handler.py 130 | 131 | # Test Google Forms API 132 | python -m unittest tests/test_forms_api.py 133 | 134 | # Test agent integration 135 | python -m unittest tests/test_agent_integration.py 136 | ``` 137 | 138 | ### Integration Testing 139 | 140 | Test the entire system: 141 | 142 | ```bash 143 | # Start the servers 144 | docker-compose up -d 145 | 146 | # Run integration tests 147 | python -m unittest tests/test_integration.py 148 | ``` 149 | 150 | ## Performance Considerations 151 | 152 | - **Caching**: Implement caching for frequently accessed forms data 153 | - **Rate Limiting**: Be aware of Google Forms API rate limits 154 | - **Error Handling**: Implement robust error handling and retry logic 155 | - **Load Testing**: Use tools like Locust to test system performance 156 | 157 | ## Security Best Practices 158 | 159 | - **Credential Management**: Never commit credentials to version control 160 | - **Input Validation**: Validate all user input and MCP packets 161 | - **CORS Configuration**: Configure CORS appropriately for production 162 | - **Rate Limiting**: Implement rate limiting to prevent abuse 163 | - **Auth Tokens**: Use proper authentication for production deployments 164 | 165 | ## Deployment 166 | 167 | ### Production Deployment 168 | 169 | For production deployment: 170 | 171 | 1. Use a proper container orchestration system (Kubernetes, ECS) 172 | 2. Set up a reverse proxy (Nginx, Traefik) for TLS termination 173 | 3. Use a managed database for persistent data 174 | 4. Implement proper monitoring and logging 175 | 5. Set up CI/CD pipelines for automated testing and deployment 176 | 177 | ### Environment-Specific Configuration 178 | 179 | Create environment-specific configuration: 180 | 181 | - `config.dev.py` - Development settings 182 | - `config.test.py` - Testing settings 183 | - `config.prod.py` - Production settings 184 | 185 | ## Troubleshooting 186 | 187 | Common issues and solutions: 188 | 189 | 1. **Google API Authentication Errors**: 190 | - Verify credentials in `.env` file 191 | - Check that required API scopes are included 192 | - Ensure refresh token is valid 193 | 194 | 2. **Docker Network Issues**: 195 | - Make sure services can communicate on the network 196 | - Check port mappings in `docker-compose.yml` 197 | 198 | 3. **UI Animation Issues**: 199 | - Check browser console for JavaScript errors 200 | - Verify DOM element IDs match expected values 201 | 202 | 4. **MCP Protocol Errors**: 203 | - Validate request format against MCP schema 204 | - Check transaction IDs are being properly passed 205 | 206 | ## Contributing 207 | 208 | Please follow these guidelines when contributing: 209 | 210 | 1. Create a feature branch from `main` 211 | 2. Follow the existing code style and conventions 212 | 3. Write unit tests for new functionality 213 | 4. Document new features or changes 214 | 5. Submit a pull request with a clear description of changes ``` -------------------------------------------------------------------------------- /server/mcp_handler.py: -------------------------------------------------------------------------------- ```python 1 | import json 2 | import uuid 3 | from forms_api import GoogleFormsAPI 4 | import config 5 | 6 | class MCPHandler: 7 | """ 8 | Handler for Model Context Protocol (MCP) packets. 9 | 10 | Processes incoming MCP requests for Google Forms operations and returns 11 | appropriately formatted MCP responses. 12 | """ 13 | 14 | def __init__(self): 15 | """Initialize the MCP handler with a GoogleFormsAPI instance.""" 16 | self.forms_api = GoogleFormsAPI() 17 | self.version = config.MCP_VERSION 18 | self.tools = config.MCP_TOOLS 19 | 20 | def get_tools_schema(self): 21 | """Return the schema for all available tools.""" 22 | return { 23 | "create_form": { 24 | "description": "Creates a new Google Form", 25 | "parameters": { 26 | "title": { 27 | "type": "string", 28 | "description": "The title of the form" 29 | }, 30 | "description": { 31 | "type": "string", 32 | "description": "Optional description for the form" 33 | } 34 | }, 35 | "required": ["title"] 36 | }, 37 | "add_question": { 38 | "description": "Adds a question to an existing Google Form", 39 | "parameters": { 40 | "form_id": { 41 | "type": "string", 42 | "description": "The ID of the form to add the question to" 43 | }, 44 | "question_type": { 45 | "type": "string", 46 | "description": "The type of question (text, paragraph, multiple_choice, checkbox)", 47 | "enum": ["text", "paragraph", "multiple_choice", "checkbox"] 48 | }, 49 | "title": { 50 | "type": "string", 51 | "description": "The question title/text" 52 | }, 53 | "options": { 54 | "type": "array", 55 | "description": "Options for multiple choice or checkbox questions", 56 | "items": { 57 | "type": "string" 58 | } 59 | }, 60 | "required": { 61 | "type": "boolean", 62 | "description": "Whether the question is required", 63 | "default": False 64 | } 65 | }, 66 | "required": ["form_id", "question_type", "title"] 67 | }, 68 | "get_responses": { 69 | "description": "Gets responses for a Google Form", 70 | "parameters": { 71 | "form_id": { 72 | "type": "string", 73 | "description": "The ID of the form to get responses for" 74 | } 75 | }, 76 | "required": ["form_id"] 77 | } 78 | } 79 | 80 | def process_request(self, request_data): 81 | """ 82 | Process an incoming MCP request. 83 | 84 | Args: 85 | request_data: Dict containing the MCP request data 86 | 87 | Returns: 88 | dict: MCP response packet 89 | """ 90 | try: 91 | # Extract MCP request components 92 | transaction_id = request_data.get('transaction_id', str(uuid.uuid4())) 93 | tool_name = request_data.get('tool_name') 94 | parameters = request_data.get('parameters', {}) 95 | 96 | # Validate tool name 97 | if tool_name not in self.tools: 98 | return self._create_error_response( 99 | transaction_id, 100 | f"Unknown tool '{tool_name}'. Available tools: {', '.join(self.tools)}" 101 | ) 102 | 103 | # Process the request based on the tool name 104 | if tool_name == "create_form": 105 | return self._handle_create_form(transaction_id, parameters) 106 | elif tool_name == "add_question": 107 | return self._handle_add_question(transaction_id, parameters) 108 | elif tool_name == "get_responses": 109 | return self._handle_get_responses(transaction_id, parameters) 110 | 111 | # Shouldn't reach here due to validation above 112 | return self._create_error_response(transaction_id, f"Tool '{tool_name}' not implemented") 113 | 114 | except Exception as e: 115 | return self._create_error_response( 116 | request_data.get('transaction_id', str(uuid.uuid4())), 117 | f"Error processing request: {str(e)}" 118 | ) 119 | 120 | def _handle_create_form(self, transaction_id, parameters): 121 | """Handle a create_form MCP request.""" 122 | if 'title' not in parameters: 123 | return self._create_error_response(transaction_id, "Missing required parameter 'title'") 124 | 125 | title = parameters['title'] 126 | description = parameters.get('description', "") 127 | 128 | result = self.forms_api.create_form(title, description) 129 | 130 | return { 131 | "transaction_id": transaction_id, 132 | "status": "success", 133 | "result": result 134 | } 135 | 136 | def _handle_add_question(self, transaction_id, parameters): 137 | """Handle an add_question MCP request.""" 138 | # Validate required parameters 139 | required_params = ['form_id', 'question_type', 'title'] 140 | for param in required_params: 141 | if param not in parameters: 142 | return self._create_error_response(transaction_id, f"Missing required parameter '{param}'") 143 | 144 | # Extract parameters 145 | form_id = parameters['form_id'] 146 | question_type = parameters['question_type'] 147 | title = parameters['title'] 148 | options = parameters.get('options', []) 149 | required = parameters.get('required', False) 150 | 151 | # Validate question type 152 | valid_types = ['text', 'paragraph', 'multiple_choice', 'checkbox'] 153 | if question_type not in valid_types: 154 | return self._create_error_response( 155 | transaction_id, 156 | f"Invalid question_type '{question_type}'. Valid types: {', '.join(valid_types)}" 157 | ) 158 | 159 | # Validate options for choice questions 160 | if question_type in ['multiple_choice', 'checkbox'] and not options: 161 | return self._create_error_response( 162 | transaction_id, 163 | f"Options are required for '{question_type}' questions" 164 | ) 165 | 166 | result = self.forms_api.add_question(form_id, question_type, title, options, required) 167 | 168 | return { 169 | "transaction_id": transaction_id, 170 | "status": "success", 171 | "result": result 172 | } 173 | 174 | def _handle_get_responses(self, transaction_id, parameters): 175 | """Handle a get_responses MCP request.""" 176 | if 'form_id' not in parameters: 177 | return self._create_error_response(transaction_id, "Missing required parameter 'form_id'") 178 | 179 | form_id = parameters['form_id'] 180 | result = self.forms_api.get_responses(form_id) 181 | 182 | return { 183 | "transaction_id": transaction_id, 184 | "status": "success", 185 | "result": result 186 | } 187 | 188 | def _create_error_response(self, transaction_id, error_message): 189 | """Create an MCP error response.""" 190 | return { 191 | "transaction_id": transaction_id, 192 | "status": "error", 193 | "error": { 194 | "message": error_message 195 | } 196 | } 197 | ``` -------------------------------------------------------------------------------- /server/static/styles.css: -------------------------------------------------------------------------------- ```css 1 | /* Global Styles */ 2 | :root { 3 | --bg-color: #121212; 4 | --panel-bg: #1e1e1e; 5 | --panel-border: #333333; 6 | --text-color: #e0e0e0; 7 | --highlight-color: #00ccff; 8 | --secondary-highlight: #00ff9d; 9 | --danger-color: #ff4757; 10 | --warning-color: #ffa502; 11 | --success-color: #2ed573; 12 | --muted-color: #747d8c; 13 | --flow-line-color: #333333; 14 | --flow-node-border: #444444; 15 | --frontend-color: #00ccff; 16 | --agent-color: #00ff9d; 17 | --mcp-color: #ff9f43; 18 | --google-color: #ff6b81; 19 | } 20 | 21 | body { 22 | background-color: var(--bg-color); 23 | color: var(--text-color); 24 | font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; 25 | margin: 0; 26 | padding: 0; 27 | min-height: 100vh; 28 | } 29 | 30 | .container-fluid { 31 | padding: 20px; 32 | } 33 | 34 | /* Header Styles */ 35 | .header { 36 | display: flex; 37 | justify-content: space-between; 38 | align-items: center; 39 | padding: 15px 0; 40 | margin-bottom: 20px; 41 | border-bottom: 1px solid var(--panel-border); 42 | } 43 | 44 | .logo-container h1 { 45 | margin: 0; 46 | font-size: 1.8rem; 47 | color: var(--highlight-color); 48 | } 49 | 50 | .subtitle { 51 | color: var(--muted-color); 52 | margin-top: 5px; 53 | } 54 | 55 | .status-indicator { 56 | display: flex; 57 | align-items: center; 58 | } 59 | 60 | .status-dot { 61 | width: 10px; 62 | height: 10px; 63 | border-radius: 50%; 64 | background-color: var(--warning-color); 65 | margin-right: 8px; 66 | display: inline-block; 67 | transition: background-color 0.3s ease; 68 | } 69 | 70 | .status-dot.connected { 71 | background-color: var(--success-color); 72 | } 73 | 74 | .status-dot.error { 75 | background-color: var(--danger-color); 76 | } 77 | 78 | /* Panel Styles */ 79 | .panel { 80 | background-color: var(--panel-bg); 81 | border-radius: 8px; 82 | border: 1px solid var(--panel-border); 83 | padding: 20px; 84 | margin-bottom: 20px; 85 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 86 | height: calc(100vh - 150px); 87 | overflow-y: auto; 88 | } 89 | 90 | .panel h2 { 91 | color: var(--secondary-highlight); 92 | margin-top: 0; 93 | margin-bottom: 20px; 94 | font-size: 1.4rem; 95 | border-bottom: 1px solid var(--panel-border); 96 | padding-bottom: 10px; 97 | } 98 | 99 | /* Form Controls */ 100 | .form-group { 101 | margin-bottom: 20px; 102 | } 103 | 104 | .form-control { 105 | background-color: #2a2a2a; 106 | border: 1px solid var(--panel-border); 107 | color: var(--text-color); 108 | border-radius: 4px; 109 | } 110 | 111 | .form-control:focus { 112 | background-color: #2a2a2a; 113 | border-color: var(--highlight-color); 114 | color: var(--text-color); 115 | box-shadow: 0 0 0 0.25rem rgba(0, 204, 255, 0.25); 116 | } 117 | 118 | .btn-primary { 119 | background-color: var(--highlight-color); 120 | border-color: var(--highlight-color); 121 | color: #000; 122 | font-weight: 500; 123 | } 124 | 125 | .btn-primary:hover { 126 | background-color: #00a3cc; 127 | border-color: #00a3cc; 128 | color: #000; 129 | } 130 | 131 | .btn-outline-secondary { 132 | color: var(--text-color); 133 | border-color: var(--panel-border); 134 | } 135 | 136 | .btn-outline-secondary:hover { 137 | background-color: #2a2a2a; 138 | color: var(--highlight-color); 139 | border-color: var(--highlight-color); 140 | } 141 | 142 | .btn-outline-info, .btn-outline-warning { 143 | color: var(--text-color); 144 | } 145 | 146 | .demo-actions { 147 | margin-top: 30px; 148 | } 149 | 150 | .demo-actions h3 { 151 | font-size: 1.1rem; 152 | margin-bottom: 15px; 153 | color: var(--muted-color); 154 | } 155 | 156 | .demo-btn { 157 | margin-bottom: 10px; 158 | display: block; 159 | width: 100%; 160 | text-align: left; 161 | overflow: hidden; 162 | text-overflow: ellipsis; 163 | white-space: nowrap; 164 | } 165 | 166 | /* Flow Visualization */ 167 | .flow-panel { 168 | display: flex; 169 | flex-direction: column; 170 | align-items: center; 171 | justify-content: center; 172 | padding-top: 20px; 173 | } 174 | 175 | .flow-container { 176 | display: flex; 177 | flex-direction: column; 178 | align-items: center; 179 | justify-content: center; 180 | width: 100%; 181 | padding: 20px 0; 182 | position: relative; 183 | } 184 | 185 | .node { 186 | display: flex; 187 | flex-direction: column; 188 | align-items: center; 189 | margin: 10px 0; 190 | position: relative; 191 | z-index: 2; 192 | transition: transform 0.3s ease, filter 0.3s ease; 193 | } 194 | 195 | .node:hover { 196 | transform: scale(1.05); 197 | } 198 | 199 | .node-icon { 200 | display: flex; 201 | align-items: center; 202 | justify-content: center; 203 | width: 50px; 204 | height: 50px; 205 | border-radius: 50%; 206 | background: var(--panel-bg); 207 | border: 2px solid var(--flow-node-border); 208 | position: relative; 209 | transition: all 0.3s ease; 210 | overflow: hidden; 211 | } 212 | 213 | /* Add highlighted state */ 214 | .node.highlighted .node-icon { 215 | transform: scale(1.1); 216 | border-width: 3px; 217 | } 218 | 219 | /* Active state */ 220 | .node.active .node-icon { 221 | border-width: 3px; 222 | animation: pulse 1.5s infinite; 223 | } 224 | 225 | #frontendNode .node-icon { 226 | border-color: var(--frontend-color); 227 | } 228 | 229 | #agentNode .node-icon { 230 | border-color: var(--agent-color); 231 | } 232 | 233 | #mcpNode .node-icon { 234 | border-color: var(--mcp-color); 235 | } 236 | 237 | #googleNode .node-icon { 238 | border-color: var(--google-color); 239 | } 240 | 241 | #frontendNode.active .node-icon { 242 | border-color: var(--frontend-color); 243 | box-shadow: 0 0 15px var(--frontend-color); 244 | } 245 | 246 | #agentNode.active .node-icon { 247 | border-color: var(--agent-color); 248 | box-shadow: 0 0 15px var(--agent-color); 249 | } 250 | 251 | #mcpNode.active .node-icon { 252 | border-color: var(--mcp-color); 253 | box-shadow: 0 0 15px var(--mcp-color); 254 | } 255 | 256 | #googleNode.active .node-icon { 257 | border-color: var(--google-color); 258 | box-shadow: 0 0 15px var(--google-color); 259 | } 260 | 261 | .node-glow { 262 | position: absolute; 263 | width: 50px; 264 | height: 50px; 265 | border-radius: 50%; 266 | background: transparent; 267 | z-index: 1; 268 | } 269 | 270 | .node-label { 271 | margin-top: 8px; 272 | font-size: 0.9rem; 273 | color: var(--muted-color); 274 | transition: color 0.3s ease; 275 | } 276 | 277 | .active .node-label { 278 | color: var(--text-color); 279 | font-weight: bold; 280 | } 281 | 282 | .highlighted .node-label { 283 | color: var(--highlight-color); 284 | } 285 | 286 | .flow-line { 287 | width: 3px; 288 | height: 80px; 289 | background-color: var(--flow-line-color); 290 | position: relative; 291 | z-index: 1; 292 | transition: all 0.3s ease; 293 | } 294 | 295 | .flow-line.active { 296 | background-color: var(--highlight-color); 297 | box-shadow: 0 0 10px 2px var(--highlight-color); 298 | animation: lineGlow 1.5s infinite; 299 | } 300 | 301 | .flow-particle-container { 302 | position: absolute; 303 | top: 0; 304 | left: -2px; 305 | width: 7px; 306 | height: 100%; 307 | overflow: visible; 308 | } 309 | 310 | .flow-particle { 311 | border-radius: 50%; 312 | position: absolute; 313 | transition: top 0.8s ease-out, opacity 0.8s ease-out; 314 | } 315 | 316 | /* Error state */ 317 | .node.error .node-icon { 318 | border-color: var(--danger-color); 319 | box-shadow: 0 0 15px var(--danger-color); 320 | animation: errorPulse 1.5s infinite; 321 | } 322 | 323 | /* Result Panel */ 324 | .result-panel { 325 | height: calc(100vh - 150px); 326 | display: flex; 327 | flex-direction: column; 328 | } 329 | 330 | .form-links { 331 | margin: 15px 0; 332 | } 333 | 334 | .form-links a { 335 | margin-right: 10px; 336 | } 337 | 338 | #formResult { 339 | background-color: #252525; 340 | border-radius: 6px; 341 | padding: 15px; 342 | margin-bottom: 20px; 343 | } 344 | 345 | #formResult.hidden { 346 | display: none; 347 | } 348 | 349 | #formTitle { 350 | font-size: 1.2rem; 351 | margin-top: 0; 352 | color: var(--secondary-highlight); 353 | } 354 | 355 | .log-container { 356 | background-color: #1a1a1a; 357 | border-radius: 6px; 358 | padding: 10px; 359 | height: 100%; 360 | overflow-y: auto; 361 | font-family: 'Consolas', monospace; 362 | font-size: 0.8rem; 363 | } 364 | 365 | .packet-entry { 366 | margin-bottom: 15px; 367 | border-bottom: 1px solid var(--panel-border); 368 | padding-bottom: 10px; 369 | } 370 | 371 | .packet-header { 372 | display: flex; 373 | justify-content: space-between; 374 | margin-bottom: 5px; 375 | font-size: 0.7rem; 376 | } 377 | 378 | .transaction-id { 379 | color: var(--highlight-color); 380 | } 381 | 382 | .packet-time { 383 | color: var(--muted-color); 384 | } 385 | 386 | .packet-direction { 387 | font-weight: bold; 388 | } 389 | 390 | .packet-direction.request { 391 | color: var(--secondary-highlight); 392 | } 393 | 394 | .packet-direction.response { 395 | color: var(--warning-color); 396 | } 397 | 398 | .packet-content { 399 | margin: 0; 400 | white-space: pre-wrap; 401 | color: #bbb; 402 | font-size: 0.7rem; 403 | max-height: 200px; 404 | overflow-y: auto; 405 | } 406 | 407 | /* Animations */ 408 | @keyframes pulse { 409 | 0% { 410 | transform: scale(1); 411 | box-shadow: 0 0 0 0 rgba(0, 204, 255, 0.7); 412 | } 413 | 50% { 414 | transform: scale(1.05); 415 | box-shadow: 0 0 0 10px rgba(0, 204, 255, 0); 416 | } 417 | 100% { 418 | transform: scale(1); 419 | box-shadow: 0 0 0 0 rgba(0, 204, 255, 0.7); 420 | } 421 | } 422 | 423 | @keyframes errorPulse { 424 | 0% { 425 | transform: scale(1); 426 | box-shadow: 0 0 0 0 rgba(255, 71, 87, 0.7); 427 | } 428 | 50% { 429 | transform: scale(1.05); 430 | box-shadow: 0 0 0 10px rgba(255, 71, 87, 0); 431 | } 432 | 100% { 433 | transform: scale(1); 434 | box-shadow: 0 0 0 0 rgba(255, 71, 87, 0.7); 435 | } 436 | } 437 | 438 | @keyframes lineGlow { 439 | 0% { 440 | opacity: 0.7; 441 | box-shadow: 0 0 5px 1px var(--highlight-color); 442 | } 443 | 50% { 444 | opacity: 1; 445 | box-shadow: 0 0 10px 2px var(--highlight-color); 446 | } 447 | 100% { 448 | opacity: 0.7; 449 | box-shadow: 0 0 5px 1px var(--highlight-color); 450 | } 451 | } 452 | 453 | .active .node-icon { 454 | border-color: var(--highlight-color); 455 | } 456 | 457 | .active .node-glow { 458 | animation: glow 1.5s infinite; 459 | } 460 | 461 | .node.pulse { 462 | animation: nodePulse 1s infinite; 463 | } 464 | 465 | /* List group customization */ 466 | .list-group-item { 467 | background-color: #252525; 468 | color: var(--text-color); 469 | border-color: var(--panel-border); 470 | } 471 | 472 | .packet-log-entry .packet-direction.agent-step { 473 | background-color: #e8f0fe; /* Light blue background */ 474 | color: #1a73e8; /* Google blue text */ 475 | border: 1px solid #d2e3fc; 476 | } 477 | 478 | .packet-log-entry .packet-direction.user-request { 479 | background-color: #fef7e0; /* Light yellow */ 480 | color: #ea8600; /* Amber */ 481 | border: 1px solid #fcefc9; 482 | } 483 | 484 | .packet-log-entry .packet-direction.error-log { 485 | background-color: #fce8e6; /* Light red */ 486 | color: #d93025; /* Google red */ 487 | border: 1px solid #f9d6d3; 488 | } 489 | 490 | /* Add styles for the stage indicator */ 491 | .stage-indicator { 492 | background-color: #252525; 493 | border-radius: 4px; 494 | padding: 10px; 495 | margin-bottom: 15px; 496 | text-align: center; 497 | border: 1px solid var(--panel-border); 498 | } 499 | 500 | #stageIndicator { 501 | font-weight: 500; 502 | color: var(--highlight-color); 503 | } 504 | 505 | /* Add styles for highlighted nodes */ 506 | .node.highlighted { 507 | transform: scale(1.08); 508 | } 509 | 510 | .node.highlighted .node-icon { 511 | border-width: 3px; 512 | border-color: var(--highlight-color); 513 | box-shadow: 0 0 10px var(--highlight-color); 514 | } 515 | 516 | .node.highlighted .node-label { 517 | color: var(--highlight-color); 518 | font-weight: 500; 519 | } 520 | ``` -------------------------------------------------------------------------------- /server/static/animations.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Flow Animation Module for Google Forms MCP Server 3 | * Manages the visual animations of data flowing between components 4 | */ 5 | 6 | class FlowAnimator { 7 | constructor() { 8 | // Flow nodes 9 | this.nodes = { 10 | frontend: document.getElementById('frontendNode'), 11 | agent: document.getElementById('agentNode'), 12 | mcp: document.getElementById('mcpNode'), 13 | google: document.getElementById('googleNode') 14 | }; 15 | 16 | // Flow lines 17 | this.lines = { 18 | frontendToAgent: document.getElementById('frontendToAgent'), 19 | agentToMCP: document.getElementById('agentToMCP'), 20 | mcpToGoogle: document.getElementById('mcpToGoogle') 21 | }; 22 | 23 | // Particle colors 24 | this.colors = { 25 | outgoing: '#00ccff', // Cyan for outgoing requests 26 | incoming: '#00ff9d', // Green for incoming responses 27 | error: '#ff4757' // Red for errors 28 | }; 29 | 30 | // Animation state 31 | this.activeAnimations = []; 32 | this.isAnimating = false; 33 | this.currentActiveNode = null; 34 | this.currentActiveLine = null; 35 | 36 | // Animation intervals for continuous particle generation 37 | this.particleIntervals = {}; 38 | } 39 | 40 | /** 41 | * Activate a node with a glowing effect 42 | * @param {string} nodeName - The name of the node to activate 43 | */ 44 | activateNode(nodeName) { 45 | if (this.nodes[nodeName]) { 46 | // Deactivate previous node if exists 47 | if (this.currentActiveNode && this.nodes[this.currentActiveNode]) { 48 | this.nodes[this.currentActiveNode].classList.remove('active'); 49 | } 50 | 51 | // Add active class to the node 52 | this.nodes[nodeName].classList.add('active'); 53 | this.currentActiveNode = nodeName; 54 | 55 | // Return a cleanup function 56 | return () => { 57 | // Only remove active class if this is still the current active node 58 | if (this.currentActiveNode === nodeName) { 59 | this.nodes[nodeName].classList.remove('active'); 60 | this.currentActiveNode = null; 61 | } 62 | }; 63 | } 64 | return () => {}; 65 | } 66 | 67 | /** 68 | * Activate a flow line 69 | * @param {string} lineName - The name of the line to activate 70 | */ 71 | activateLine(lineName) { 72 | if (this.lines[lineName]) { 73 | // Deactivate previous line if exists 74 | if (this.currentActiveLine && this.lines[this.currentActiveLine]) { 75 | this.lines[this.currentActiveLine].classList.remove('active'); 76 | } 77 | 78 | // Add active class to the line 79 | this.lines[lineName].classList.add('active'); 80 | this.currentActiveLine = lineName; 81 | 82 | // Return a cleanup function 83 | return () => { 84 | // Only remove active class if this is still the current active line 85 | if (this.currentActiveLine === lineName) { 86 | this.lines[lineName].classList.remove('active'); 87 | this.currentActiveLine = null; 88 | } 89 | }; 90 | } 91 | return () => {}; 92 | } 93 | 94 | /** 95 | * Create and animate a particle flowing through a line 96 | * @param {string} lineName - The name of the line to animate 97 | * @param {string} direction - 'outgoing' or 'incoming' to determine color and direction 98 | * @param {number} duration - Animation duration in milliseconds 99 | */ 100 | createParticle(lineName, direction, duration = 800) { 101 | const line = this.lines[lineName]; 102 | if (!line) return null; 103 | 104 | const container = line.querySelector('.flow-particle-container'); 105 | if (!container) return null; 106 | 107 | // Create particle element 108 | const particle = document.createElement('div'); 109 | particle.className = 'flow-particle'; 110 | particle.style.position = 'absolute'; 111 | particle.style.width = '7px'; 112 | particle.style.height = '7px'; 113 | particle.style.borderRadius = '50%'; 114 | particle.style.backgroundColor = this.colors[direction] || this.colors.outgoing; 115 | particle.style.boxShadow = `0 0 8px 2px ${this.colors[direction] || this.colors.outgoing}`; 116 | 117 | // Set starting position based on direction 118 | if (direction === 'incoming') { 119 | particle.style.top = 'calc(100% - 7px)'; 120 | particle.style.transform = 'translateY(0)'; 121 | } else { 122 | particle.style.top = '0'; 123 | particle.style.transform = 'translateY(0)'; 124 | } 125 | 126 | // Add particle to container 127 | container.appendChild(particle); 128 | 129 | // Animate the particle 130 | const animation = particle.animate([ 131 | { 132 | top: direction === 'incoming' ? 'calc(100% - 7px)' : '0', 133 | opacity: 1 134 | }, 135 | { 136 | top: direction === 'incoming' ? '0' : 'calc(100% - 7px)', 137 | opacity: 0.8 138 | } 139 | ], { 140 | duration: duration, 141 | easing: 'ease-out', 142 | fill: 'forwards' 143 | }); 144 | 145 | // Remove particle when animation completes 146 | animation.onfinish = () => { 147 | if (container.contains(particle)) { 148 | container.removeChild(particle); 149 | } 150 | }; 151 | 152 | return animation; 153 | } 154 | 155 | /** 156 | * Start continuous particle animation on a line 157 | * @param {string} lineName - The line to animate 158 | * @param {string} direction - Direction of flow 159 | */ 160 | startContinuousParticles(lineName, direction) { 161 | // Clear any existing interval for this line 162 | this.stopContinuousParticles(lineName); 163 | 164 | // Create new interval 165 | const interval = setInterval(() => { 166 | this.createParticle(lineName, direction, 800); 167 | }, 200); 168 | 169 | // Store the interval 170 | this.particleIntervals[lineName] = interval; 171 | } 172 | 173 | /** 174 | * Stop continuous particle animation on a line 175 | * @param {string} lineName - The line to stop animating 176 | */ 177 | stopContinuousParticles(lineName) { 178 | if (this.particleIntervals[lineName]) { 179 | clearInterval(this.particleIntervals[lineName]); 180 | delete this.particleIntervals[lineName]; 181 | 182 | // Clear any remaining particles 183 | const line = this.lines[lineName]; 184 | if (line) { 185 | const container = line.querySelector('.flow-particle-container'); 186 | if (container) { 187 | container.innerHTML = ''; 188 | } 189 | } 190 | } 191 | } 192 | 193 | /** 194 | * Stop all continuous particle animations 195 | */ 196 | stopAllContinuousParticles() { 197 | Object.keys(this.particleIntervals).forEach(lineName => { 198 | this.stopContinuousParticles(lineName); 199 | }); 200 | } 201 | 202 | /** 203 | * Animate flow from one node to another 204 | * @param {string} fromNode - Source node name 205 | * @param {string} toNode - Target node name 206 | * @param {string} direction - 'outgoing' or 'incoming' 207 | * @returns {Promise} - Resolves when animation completes 208 | */ 209 | async animateFlow(fromNode, toNode, direction = 'outgoing') { 210 | // Define flow paths 211 | const flowPaths = { 212 | 'frontend-agent': 'frontendToAgent', 213 | 'agent-mcp': 'agentToMCP', 214 | 'mcp-google': 'mcpToGoogle', 215 | 'google-mcp': 'mcpToGoogle', 216 | 'mcp-agent': 'agentToMCP', 217 | 'agent-frontend': 'frontendToAgent' 218 | }; 219 | 220 | const pathKey = `${fromNode}-${toNode}`; 221 | const lineName = flowPaths[pathKey]; 222 | 223 | if (!lineName) { 224 | console.error(`No flow path defined for ${pathKey}`); 225 | return; 226 | } 227 | 228 | // Stop any existing continuous animations 229 | this.stopAllContinuousParticles(); 230 | 231 | // Activate source node 232 | const cleanupSource = this.activateNode(fromNode); 233 | 234 | // Activate the flow line 235 | const cleanupLine = this.activateLine(lineName); 236 | 237 | // Start continuous particles 238 | this.startContinuousParticles(lineName, direction); 239 | 240 | // Create promise that resolves when animation completes 241 | return new Promise(resolve => { 242 | setTimeout(() => { 243 | // Activate target node 244 | const cleanupTarget = this.activateNode(toNode); 245 | 246 | // Stop continuous particles 247 | this.stopContinuousParticles(lineName); 248 | 249 | // Cleanup source node after delay 250 | cleanupSource(); 251 | 252 | // Cleanup line 253 | cleanupLine(); 254 | 255 | // Resolve the promise 256 | resolve(); 257 | }, 1500); // Longer wait to show the flow more clearly 258 | }); 259 | } 260 | 261 | /** 262 | * Animate a complete request-response flow 263 | * @param {string} scenario - The flow scenario to animate 264 | */ 265 | async animateRequestResponseFlow(scenario = 'form-creation') { 266 | // Prevent multiple animations 267 | if (this.isAnimating) return; 268 | this.isAnimating = true; 269 | 270 | try { 271 | // Common flow for all scenarios 272 | // Frontend -> Agent -> MCP -> Google -> MCP -> Agent -> Frontend 273 | 274 | // Request flow 275 | await this.animateFlow('frontend', 'agent', 'outgoing'); 276 | await this.animateFlow('agent', 'mcp', 'outgoing'); 277 | await this.animateFlow('mcp', 'google', 'outgoing'); 278 | 279 | // Response flow 280 | await this.animateFlow('google', 'mcp', 'incoming'); 281 | await this.animateFlow('mcp', 'agent', 'incoming'); 282 | await this.animateFlow('agent', 'frontend', 'incoming'); 283 | 284 | } catch (error) { 285 | console.error('Animation error:', error); 286 | } finally { 287 | this.isAnimating = false; 288 | } 289 | } 290 | 291 | /** 292 | * Animate a flow with an error 293 | * @param {string} errorStage - The stage where the error occurs 294 | */ 295 | async animateErrorFlow(errorStage = 'google') { 296 | if (this.isAnimating) return; 297 | this.isAnimating = true; 298 | 299 | try { 300 | // Initial flow 301 | await this.animateFlow('frontend', 'agent', 'outgoing'); 302 | 303 | if (errorStage === 'agent') { 304 | // Error at agent 305 | this.nodes.agent.classList.add('error'); 306 | setTimeout(() => { 307 | this.nodes.agent.classList.remove('error'); 308 | this.nodes.agent.classList.remove('active'); 309 | }, 2000); 310 | return; 311 | } 312 | 313 | await this.animateFlow('agent', 'mcp', 'outgoing'); 314 | 315 | if (errorStage === 'mcp') { 316 | // Error at MCP server 317 | this.nodes.mcp.classList.add('error'); 318 | setTimeout(() => { 319 | this.nodes.mcp.classList.remove('error'); 320 | this.nodes.mcp.classList.remove('active'); 321 | }, 2000); 322 | return; 323 | } 324 | 325 | await this.animateFlow('mcp', 'google', 'outgoing'); 326 | 327 | if (errorStage === 'google') { 328 | // Error at Google Forms API 329 | this.nodes.google.classList.add('error'); 330 | setTimeout(() => { 331 | this.nodes.google.classList.remove('error'); 332 | this.nodes.google.classList.remove('active'); 333 | 334 | // Error response flow 335 | this.animateFlow('google', 'mcp', 'error'); 336 | this.animateFlow('mcp', 'agent', 'error'); 337 | this.animateFlow('agent', 'frontend', 'error'); 338 | }, 2000); 339 | } 340 | 341 | } catch (error) { 342 | console.error('Error animation error:', error); 343 | } finally { 344 | setTimeout(() => { 345 | this.isAnimating = false; 346 | }, 3000); 347 | } 348 | } 349 | 350 | /** 351 | * Reset all animations and active states 352 | */ 353 | resetAll() { 354 | // Stop all continuous particles 355 | this.stopAllContinuousParticles(); 356 | 357 | // Reset nodes 358 | Object.values(this.nodes).forEach(node => { 359 | node.classList.remove('active'); 360 | node.classList.remove('error'); 361 | node.classList.remove('pulse'); 362 | }); 363 | 364 | // Reset lines 365 | Object.values(this.lines).forEach(line => { 366 | line.classList.remove('active'); 367 | const container = line.querySelector('.flow-particle-container'); 368 | if (container) { 369 | container.innerHTML = ''; 370 | } 371 | }); 372 | 373 | this.currentActiveNode = null; 374 | this.currentActiveLine = null; 375 | this.isAnimating = false; 376 | } 377 | 378 | /** 379 | * Pulse animation for a specific node 380 | * @param {string} nodeName - Name of the node to pulse 381 | * @param {number} duration - Duration in milliseconds 382 | */ 383 | pulseNode(nodeName, duration = 2000) { 384 | const node = this.nodes[nodeName]; 385 | if (!node) return; 386 | 387 | node.classList.add('pulse'); 388 | 389 | setTimeout(() => { 390 | node.classList.remove('pulse'); 391 | }, duration); 392 | } 393 | 394 | /** 395 | * Highlight a specific node to show it's the current active component 396 | * @param {string} nodeName - Name of the node to highlight 397 | */ 398 | highlightNode(nodeName) { 399 | // First clear any existing highlights 400 | Object.keys(this.nodes).forEach(name => { 401 | this.nodes[name].classList.remove('highlighted'); 402 | }); 403 | 404 | // Set the new highlight 405 | if (this.nodes[nodeName]) { 406 | this.nodes[nodeName].classList.add('highlighted'); 407 | } 408 | } 409 | } 410 | 411 | // Initialize flow animator when document loads 412 | document.addEventListener('DOMContentLoaded', function() { 413 | window.flowAnimator = new FlowAnimator(); 414 | 415 | // For demo purposes, animate the request flow on load to demonstrate functionality 416 | setTimeout(() => { 417 | if (window.flowAnimator) { 418 | window.flowAnimator.animateRequestResponseFlow(); 419 | } 420 | }, 2000); 421 | }); 422 | ``` -------------------------------------------------------------------------------- /server/static/main.js: -------------------------------------------------------------------------------- ```javascript 1 | /** 2 | * Google Forms MCP Server 3 | * Main JavaScript file for handling server interactions, UI updates, and flow visualization 4 | */ 5 | 6 | // State management 7 | const state = { 8 | isConnected: false, 9 | currentForm: null, 10 | currentTransaction: null, 11 | requestInProgress: false, 12 | currentStage: null, // Tracks the current stage in the flow 13 | questions: [] 14 | }; 15 | 16 | // Stages in the flow 17 | const STAGES = { 18 | IDLE: 'idle', 19 | FRONTEND: 'frontend', 20 | AGENT: 'agent', 21 | MCP: 'mcp', 22 | GOOGLE: 'google', 23 | COMPLETE: 'complete', 24 | ERROR: 'error' 25 | }; 26 | 27 | // API endpoints 28 | const API = { 29 | health: '/api/health', 30 | form_request: '/api/form_request', 31 | agent_proxy: '/api/agent_proxy', 32 | form_status: '/api/form_status' 33 | }; 34 | 35 | // DOM Elements 36 | const elements = { 37 | statusDot: document.getElementById('statusDot'), 38 | statusText: document.getElementById('statusText'), 39 | requestInput: document.getElementById('requestInput'), 40 | sendRequestBtn: document.getElementById('sendRequestBtn'), 41 | formResult: document.getElementById('formResult'), 42 | formTitle: document.getElementById('formTitle'), 43 | formViewLink: document.getElementById('formViewLink'), 44 | formEditLink: document.getElementById('formEditLink'), 45 | questionsList: document.getElementById('questionsList'), 46 | packetLog: document.getElementById('packetLog'), 47 | demoBtns: document.querySelectorAll('.demo-btn'), 48 | stageIndicator: document.getElementById('stageIndicator') 49 | }; 50 | 51 | // Templates 52 | const templates = { 53 | packetEntry: document.getElementById('packetTemplate') 54 | }; 55 | 56 | /** 57 | * Initialize the application 58 | */ 59 | function init() { 60 | // Set up event listeners 61 | setupEventListeners(); 62 | 63 | // Check server connection 64 | checkServerConnection(); 65 | 66 | // Initialize stage 67 | updateStage(STAGES.IDLE); 68 | 69 | // Pulse frontend node on initial load to indicate entry point 70 | setTimeout(() => { 71 | if (window.flowAnimator) { 72 | window.flowAnimator.pulseNode('frontend', 3000); 73 | } 74 | }, 1000); 75 | } 76 | 77 | /** 78 | * Update the current stage in the flow 79 | * @param {string} stage - The current stage 80 | */ 81 | function updateStage(stage) { 82 | state.currentStage = stage; 83 | 84 | // Update visual indication of current stage 85 | if (window.flowAnimator) { 86 | // Highlight the appropriate node based on stage 87 | switch (stage) { 88 | case STAGES.FRONTEND: 89 | window.flowAnimator.highlightNode('frontend'); 90 | break; 91 | case STAGES.AGENT: 92 | window.flowAnimator.highlightNode('agent'); 93 | break; 94 | case STAGES.MCP: 95 | window.flowAnimator.highlightNode('mcp'); 96 | break; 97 | case STAGES.GOOGLE: 98 | window.flowAnimator.highlightNode('google'); 99 | break; 100 | default: 101 | // Clear highlights for IDLE, COMPLETE, ERROR 102 | Object.keys(window.flowAnimator.nodes).forEach(nodeName => { 103 | window.flowAnimator.nodes[nodeName].classList.remove('highlighted'); 104 | }); 105 | break; 106 | } 107 | } 108 | 109 | // Update stage indicator text if present 110 | if (elements.stageIndicator) { 111 | let stageText = ''; 112 | switch (stage) { 113 | case STAGES.IDLE: 114 | stageText = 'Ready for request'; 115 | break; 116 | case STAGES.FRONTEND: 117 | stageText = 'Processing in frontend'; 118 | break; 119 | case STAGES.AGENT: 120 | stageText = 'Agent processing request'; 121 | break; 122 | case STAGES.MCP: 123 | stageText = 'MCP Server building form'; 124 | break; 125 | case STAGES.GOOGLE: 126 | stageText = 'Interacting with Google Forms'; 127 | break; 128 | case STAGES.COMPLETE: 129 | stageText = 'Form creation complete'; 130 | break; 131 | case STAGES.ERROR: 132 | stageText = 'Error occurred'; 133 | break; 134 | default: 135 | stageText = 'Processing...'; 136 | } 137 | elements.stageIndicator.textContent = stageText; 138 | } 139 | } 140 | 141 | /** 142 | * Set up event listeners 143 | */ 144 | function setupEventListeners() { 145 | if (elements.sendRequestBtn) { 146 | elements.sendRequestBtn.addEventListener('click', handleSendRequest); 147 | } 148 | 149 | // Demo buttons 150 | elements.demoBtns.forEach(btn => { 151 | btn.addEventListener('click', function() { 152 | const requestText = this.getAttribute('data-request'); 153 | if (elements.requestInput && requestText) { 154 | elements.requestInput.value = requestText; 155 | // Automatically trigger request after a short delay 156 | setTimeout(() => { 157 | handleSendRequest(); 158 | }, 500); 159 | } 160 | }); 161 | }); 162 | } 163 | 164 | /** 165 | * Check if server is connected 166 | */ 167 | async function checkServerConnection() { 168 | try { 169 | const response = await fetch(API.health); 170 | if (response.ok) { 171 | const data = await response.json(); 172 | updateConnectionStatus(true, `Connected (v${data.version})`); 173 | return true; 174 | } else { 175 | updateConnectionStatus(false, 'Server Error'); 176 | return false; 177 | } 178 | } catch (error) { 179 | console.error('Server connection error:', error); 180 | updateConnectionStatus(false, 'Disconnected'); 181 | return false; 182 | } 183 | } 184 | 185 | /** 186 | * Update connection status indicator 187 | */ 188 | function updateConnectionStatus(isConnected, statusMessage) { 189 | state.isConnected = isConnected; 190 | 191 | if (elements.statusDot) { 192 | elements.statusDot.className = 'status-dot ' + (isConnected ? 'connected' : 'error'); 193 | } 194 | 195 | if (elements.statusText) { 196 | elements.statusText.textContent = statusMessage || (isConnected ? 'Connected' : 'Disconnected'); 197 | } 198 | } 199 | 200 | /** 201 | * Handle the form request submission 202 | */ 203 | async function handleSendRequest() { 204 | // Validation & state check 205 | if (!elements.requestInput || !elements.requestInput.value.trim()) { 206 | alert('Please enter a request'); 207 | return; 208 | } 209 | 210 | if (state.requestInProgress) { 211 | return; 212 | } 213 | 214 | // Reset any previous results 215 | resetResults(); 216 | 217 | // Update UI 218 | state.requestInProgress = true; 219 | elements.sendRequestBtn.disabled = true; 220 | elements.sendRequestBtn.textContent = 'Processing...'; 221 | 222 | // Get the request text 223 | const requestText = elements.requestInput.value.trim(); 224 | 225 | // Log the user request 226 | logPacket({ request_text: requestText }, 'User Request'); 227 | 228 | try { 229 | // Start at frontend 230 | updateStage(STAGES.FRONTEND); 231 | 232 | // Pulse frontend node to start the flow 233 | window.flowAnimator.pulseNode('frontend', 1000); 234 | 235 | // Animate the request flow from frontend to agent 236 | await window.flowAnimator.animateFlow('frontend', 'agent', 'outgoing'); 237 | 238 | // Update stage to agent 239 | updateStage(STAGES.AGENT); 240 | 241 | // Process the request with the agent 242 | const agentResponse = await processWithAgent(requestText); 243 | 244 | // Log agent proxy response 245 | logPacket(agentResponse, 'Agent Processing'); 246 | 247 | if (agentResponse.status === 'error') { 248 | // Show error animation in the flow diagram 249 | window.flowAnimator.animateErrorFlow('agent'); 250 | updateStage(STAGES.ERROR); 251 | throw new Error(agentResponse.message || 'Agent processing failed'); 252 | } 253 | 254 | // Continue flow to MCP server 255 | await window.flowAnimator.animateFlow('agent', 'mcp', 'outgoing'); 256 | updateStage(STAGES.MCP); 257 | 258 | // Simulate MCP server processing time 259 | await new Promise(resolve => setTimeout(resolve, 1000)); 260 | 261 | // Continue flow to Google Forms 262 | await window.flowAnimator.animateFlow('mcp', 'google', 'outgoing'); 263 | updateStage(STAGES.GOOGLE); 264 | 265 | // Simulate Google Forms API interaction time 266 | await new Promise(resolve => setTimeout(resolve, 1500)); 267 | 268 | // Begin response flow 269 | // From Google back to MCP 270 | await window.flowAnimator.animateFlow('google', 'mcp', 'incoming'); 271 | updateStage(STAGES.MCP); 272 | 273 | // From MCP back to agent 274 | await window.flowAnimator.animateFlow('mcp', 'agent', 'incoming'); 275 | updateStage(STAGES.AGENT); 276 | 277 | // Final response from agent to frontend 278 | await window.flowAnimator.animateFlow('agent', 'frontend', 'incoming'); 279 | updateStage(STAGES.FRONTEND); 280 | 281 | // Check agent response status 282 | if (agentResponse.status === 'success' && agentResponse.result) { 283 | if (agentResponse.result.form_id) { 284 | // Log the final successful response from the agent/MCP flow 285 | logPacket(agentResponse, 'Final Response'); 286 | 287 | // Update the UI with the final form details 288 | updateFormResult(agentResponse.result, agentResponse.result.questions || []); 289 | 290 | // Update stage to complete 291 | updateStage(STAGES.COMPLETE); 292 | } else { 293 | logPacket(agentResponse, 'Agent Response (No Form)'); 294 | alert('Agent processed the request, but no form details were returned.'); 295 | updateStage(STAGES.ERROR); 296 | } 297 | } else { 298 | // Log the error response from the agent 299 | logPacket(agentResponse, 'Agent Error'); 300 | 301 | // Show error animation in the flow diagram 302 | window.flowAnimator.animateErrorFlow('agent'); 303 | updateStage(STAGES.ERROR); 304 | 305 | throw new Error(agentResponse.message || 'Agent processing failed'); 306 | } 307 | 308 | } catch (error) { 309 | console.error('Error during form creation process:', error); 310 | alert(`An error occurred: ${error.message}`); 311 | // Log error packet if possible 312 | logPacket({ error: error.message }, 'Processing Error'); 313 | updateStage(STAGES.ERROR); 314 | } finally { 315 | elements.sendRequestBtn.disabled = false; 316 | elements.sendRequestBtn.textContent = 'Process Request'; 317 | state.requestInProgress = false; 318 | 319 | // Reset flow animator if there was an error 320 | if (state.currentStage === STAGES.ERROR) { 321 | setTimeout(() => { 322 | if (window.flowAnimator) { 323 | window.flowAnimator.resetAll(); 324 | updateStage(STAGES.IDLE); 325 | } 326 | }, 3000); 327 | } 328 | } 329 | } 330 | 331 | /** 332 | * Sends the natural language request to the agent server. 333 | * @param {string} requestText - The raw natural language text. 334 | * @returns {Promise<Object>} - The response from the agent server. 335 | */ 336 | async function processWithAgent(requestText) { 337 | console.log(`Sending request via proxy to agent: ${requestText}`); // Debug log 338 | try { 339 | const response = await fetch(API.agent_proxy, { // Use the PROXY endpoint 340 | method: 'POST', 341 | headers: { 342 | 'Content-Type': 'application/json' 343 | }, 344 | body: JSON.stringify({ request_text: requestText }) 345 | }); 346 | 347 | // Log raw response status 348 | console.log(`Agent response status: ${response.status}`); 349 | 350 | if (!response.ok) { 351 | let errorData; 352 | try { 353 | errorData = await response.json(); 354 | } catch (e) { 355 | errorData = { message: await response.text() || 'Failed to parse error response' }; 356 | } 357 | console.error('Agent API Error:', response.status, errorData); 358 | // Try to construct a meaningful error message 359 | let errorMsg = `Agent request failed: ${response.status} ${response.statusText || ''}`; 360 | if (errorData && errorData.message) { 361 | errorMsg += ` - ${errorData.message}`; 362 | } 363 | throw new Error(errorMsg); 364 | } 365 | 366 | const responseData = await response.json(); 367 | console.log('Agent response data:', responseData); // Debug log 368 | return responseData; 369 | 370 | } catch (error) { 371 | console.error('Error sending request to agent:', error); 372 | // Return a structured error object for the UI handler 373 | return { 374 | status: 'error', 375 | message: error.message || 'Failed to communicate with agent server' 376 | }; 377 | } 378 | } 379 | 380 | /** 381 | * Create an MCP packet for a tool call 382 | * @param {string} toolName - Name of the MCP tool to call 383 | * @param {Object} parameters - Parameters for the tool 384 | * @returns {Object} - Formatted MCP packet 385 | */ 386 | function createMCPPacket(toolName, parameters) { 387 | return { 388 | transaction_id: 'tx_' + Math.random().toString(36).substr(2, 9), 389 | tool_name: toolName, 390 | parameters: parameters 391 | }; 392 | } 393 | 394 | /** 395 | * Log an MCP packet or Agent Step to the UI 396 | * @param {Object} item - The packet or log entry object 397 | * @param {string} type - 'MCP Request', 'MCP Response', 'Agent Step', etc. 398 | */ 399 | function logItem(item, type) { 400 | // Clone the template 401 | const template = templates.packetEntry.content.cloneNode(true); 402 | 403 | // Set the data 404 | template.querySelector('.transaction-id').textContent = item.transaction_id || item.step_type || 'N/A'; 405 | template.querySelector('.packet-time').textContent = item.timestamp ? new Date(item.timestamp).toLocaleTimeString() : new Date().toLocaleTimeString(); 406 | 407 | const directionEl = template.querySelector('.packet-direction'); 408 | directionEl.textContent = type; 409 | 410 | // Add specific classes for styling 411 | if (type.includes('Request')) { 412 | directionEl.classList.add('request'); 413 | } else if (type.includes('Response')) { 414 | directionEl.classList.add('response'); 415 | } else if (type.includes('Agent')) { 416 | directionEl.classList.add('agent-step'); // Add a class for agent steps 417 | } else if (type.includes('User')) { 418 | directionEl.classList.add('user-request'); 419 | } else if (type.includes('Error')) { 420 | directionEl.classList.add('error-log'); 421 | } 422 | 423 | // Format the content (use item.data for agent steps) 424 | const contentToDisplay = type === 'Agent Step' ? item.data : item; 425 | template.querySelector('.packet-content').textContent = JSON.stringify(contentToDisplay, null, 2); 426 | 427 | // Add to the log 428 | elements.packetLog.prepend(template); 429 | } 430 | 431 | /** 432 | * Logs agent processing steps provided by the backend. 433 | * @param {Array<Object>} logEntries - Array of log entry objects from the agent. 434 | */ 435 | function logAgentSteps(logEntries) { 436 | // Log entries in reverse order so newest appear first in the UI log 437 | // Or log them sequentially as they happened? 438 | // Let's log sequentially as they happened for chronological understanding. 439 | logEntries.forEach(entry => { 440 | logItem(entry, 'Agent Step'); 441 | }); 442 | } 443 | 444 | /** 445 | * Modify the old logPacket function to use logItem 446 | */ 447 | function logPacket(packet, direction) { 448 | logItem(packet, direction); 449 | } 450 | 451 | /** 452 | * Reset the results UI 453 | */ 454 | function resetResults() { 455 | elements.formResult.classList.add('hidden'); 456 | elements.questionsList.innerHTML = ''; 457 | state.currentForm = null; 458 | state.currentTransaction = null; 459 | state.questions = []; 460 | 461 | // Reset animation state 462 | if (window.flowAnimator) { 463 | window.flowAnimator.resetAll(); 464 | } 465 | 466 | // Reset to idle state 467 | updateStage(STAGES.IDLE); 468 | } 469 | 470 | /** 471 | * Update the form result UI 472 | * @param {Object} formData - Form data from the API 473 | * @param {Array} questions - Questions to display 474 | */ 475 | function updateFormResult(formData, questions) { 476 | state.currentForm = formData; 477 | 478 | elements.formTitle.textContent = formData.title; 479 | elements.formViewLink.href = formData.response_url; 480 | elements.formEditLink.href = formData.edit_url; 481 | 482 | // Add questions to the list 483 | elements.questionsList.innerHTML = ''; 484 | questions.forEach(question => { 485 | const li = document.createElement('li'); 486 | li.className = 'list-group-item'; 487 | 488 | let questionText = `<strong>${question.title}</strong><br>`; 489 | questionText += `Type: ${question.type}`; 490 | 491 | if (question.options && question.options.length > 0) { 492 | questionText += `<br>Options: ${question.options.join(', ')}`; 493 | } 494 | 495 | li.innerHTML = questionText; 496 | elements.questionsList.appendChild(li); 497 | }); 498 | 499 | // Show the form result 500 | elements.formResult.classList.remove('hidden'); 501 | 502 | // Pulse frontend node to indicate completion 503 | window.flowAnimator.pulseNode('frontend'); 504 | } 505 | 506 | // Initialize the application when the DOM is loaded 507 | document.addEventListener('DOMContentLoaded', init); 508 | ``` -------------------------------------------------------------------------------- /server/forms_api.py: -------------------------------------------------------------------------------- ```python 1 | import google.oauth2.credentials 2 | from googleapiclient.discovery import build 3 | from google.oauth2 import service_account 4 | from google_auth_oauthlib.flow import InstalledAppFlow 5 | from google.auth.transport.requests import Request 6 | import json 7 | import config 8 | 9 | class GoogleFormsAPI: 10 | """ 11 | Handler for Google Forms API operations. 12 | Handles authentication and provides methods to create forms, add questions, and get responses. 13 | """ 14 | 15 | def __init__(self): 16 | self.credentials = self._get_credentials() 17 | self.forms_service = self._build_service('forms', 'v1') 18 | self.drive_service = self._build_service('drive', 'v3') 19 | 20 | def _get_credentials(self): 21 | """Create OAuth2 credentials from environment variables.""" 22 | try: 23 | print("DEBUG: Creating credentials") 24 | print(f"DEBUG: Client ID: {config.GOOGLE_CLIENT_ID[:10]}...") 25 | print(f"DEBUG: Client Secret: {config.GOOGLE_CLIENT_SECRET[:10]}...") 26 | print(f"DEBUG: Refresh Token: {config.GOOGLE_REFRESH_TOKEN[:15]}...") 27 | print(f"DEBUG: Scopes: {config.SCOPES}") 28 | 29 | credentials = google.oauth2.credentials.Credentials( 30 | token=None, # We don't have a token yet 31 | refresh_token=config.GOOGLE_REFRESH_TOKEN, 32 | client_id=config.GOOGLE_CLIENT_ID, 33 | client_secret=config.GOOGLE_CLIENT_SECRET, 34 | token_uri='https://oauth2.googleapis.com/token', 35 | scopes=[] # Start with empty scopes to avoid validation during refresh 36 | ) 37 | 38 | # Try to validate the credentials 39 | print("DEBUG: Validating credentials...") 40 | credentials.refresh(Request()) 41 | print(f"DEBUG: Credentials valid and refreshed! Token valid until: {credentials.expiry}") 42 | 43 | # Add scopes after successful refresh 44 | credentials = google.oauth2.credentials.Credentials( 45 | token=credentials.token, 46 | refresh_token=credentials.refresh_token, 47 | client_id=credentials.client_id, 48 | client_secret=credentials.client_secret, 49 | token_uri=credentials.token_uri, 50 | scopes=config.SCOPES 51 | ) 52 | 53 | return credentials 54 | except Exception as e: 55 | print(f"DEBUG: Credentials error: {str(e)}") 56 | raise 57 | 58 | def _build_service(self, api_name, version): 59 | """Build and return a Google API service.""" 60 | try: 61 | print(f"DEBUG: Building {api_name} service v{version}") 62 | service = build(api_name, version, credentials=self.credentials) 63 | print(f"DEBUG: Successfully built {api_name} service") 64 | return service 65 | except Exception as e: 66 | print(f"DEBUG: Error building {api_name} service: {str(e)}") 67 | raise 68 | 69 | def create_form(self, title, description=""): 70 | """ 71 | Create a new Google Form. 72 | 73 | Args: 74 | title: Title of the form 75 | description: Optional description for the form 76 | 77 | Returns: 78 | dict: Response containing form ID and edit URL 79 | """ 80 | try: 81 | # Debug info 82 | print("DEBUG: Starting form creation") 83 | print(f"DEBUG: Using client_id: {config.GOOGLE_CLIENT_ID[:10]}...") 84 | print(f"DEBUG: Using refresh_token: {config.GOOGLE_REFRESH_TOKEN[:10]}...") 85 | 86 | # Create a simpler form body with ONLY title as required by the API 87 | form_body = { 88 | "info": { 89 | "title": title 90 | } 91 | } 92 | 93 | # Debug info 94 | print("DEBUG: About to create form") 95 | print("DEBUG: Form body: " + str(form_body)) 96 | 97 | try: 98 | form = self.forms_service.forms().create(body=form_body).execute() 99 | form_id = form['formId'] 100 | print(f"DEBUG: Form created successfully with ID: {form_id}") 101 | print(f"DEBUG: Full form creation response: {json.dumps(form, indent=2)}") # Log full response 102 | 103 | # Get the actual URLs from the form response 104 | initial_responder_uri = form.get('responderUri') 105 | print(f"DEBUG: Initial responderUri from create response: {initial_responder_uri}") 106 | 107 | edit_url = f"https://docs.google.com/forms/d/{form_id}/edit" # Edit URL format is consistent 108 | print(f"DEBUG: Tentative Edit URL: {edit_url}") 109 | 110 | # If description is provided, update the form with it 111 | if description: 112 | print("DEBUG: Adding description through batchUpdate") 113 | update_body = { 114 | "requests": [ 115 | { 116 | "updateFormInfo": { 117 | "info": { 118 | "description": description 119 | }, 120 | "updateMask": "description" 121 | } 122 | } 123 | ] 124 | } 125 | self.forms_service.forms().batchUpdate( 126 | formId=form_id, 127 | body=update_body 128 | ).execute() 129 | print("DEBUG: Description added successfully") 130 | 131 | # Update form settings to make it public and collectable 132 | print("DEBUG: Setting form settings to make it public") 133 | settings_body = { 134 | "requests": [ 135 | { 136 | "updateSettings": { 137 | "settings": { 138 | "quizSettings": { 139 | "isQuiz": False 140 | } 141 | }, 142 | "updateMask": "quizSettings.isQuiz" 143 | } 144 | } 145 | ] 146 | } 147 | settings_response = self.forms_service.forms().batchUpdate( 148 | formId=form_id, 149 | body=settings_body 150 | ).execute() 151 | print("DEBUG: Form settings updated") 152 | print(f"DEBUG: Full settings update response: {json.dumps(settings_response, indent=2)}") # Log full response 153 | 154 | # Check if the settings response has responderUri 155 | settings_responder_uri = None 156 | if 'form' in settings_response and 'responderUri' in settings_response['form']: 157 | settings_responder_uri = settings_response['form']['responderUri'] 158 | form['responderUri'] = settings_responder_uri # Update form dict if found 159 | print(f"DEBUG: Found responderUri in settings response: {settings_responder_uri}") 160 | 161 | # Explicitly publish the form to force it to be visible - These might be redundant/incorrect 162 | # response_url = f"https://docs.google.com/forms/d/{form_id}/viewform" 163 | # edit_url = f"https://docs.google.com/forms/d/{form_id}/edit" 164 | 165 | except Exception as form_error: 166 | print(f"DEBUG: Form creation error: {str(form_error)}") 167 | # Create a mock form for testing 168 | form = { 169 | 'formId': 'form_error_state', 170 | 'responderUri': 'https://docs.google.com/forms/d/e/form_error_state/viewform' # Use /e/ format 171 | } 172 | form_id = form['formId'] 173 | print("DEBUG: Created mock form for testing") 174 | 175 | # Make the form public via Drive API - only if we have a real form 176 | try: 177 | if form_id != 'form_error_state': 178 | print(f"DEBUG: About to set Drive permissions for form {form_id}") 179 | 180 | # First try to get file to verify it exists in Drive 181 | try: 182 | file_check = self.drive_service.files().get( 183 | fileId=form_id, 184 | fields="id,name,permissions,webViewLink,webContentLink" # Added webContentLink just in case 185 | ).execute() 186 | print(f"DEBUG: File exists in Drive: {file_check.get('name', 'unknown')}") 187 | print(f"DEBUG: Full Drive file get response: {json.dumps(file_check, indent=2)}") # Log full response 188 | 189 | # Store the web view link for later use 190 | drive_web_view_link = file_check.get('webViewLink') 191 | if drive_web_view_link: 192 | print(f"DEBUG: Drive webViewLink found: {drive_web_view_link}") 193 | except Exception as file_error: 194 | print(f"DEBUG: Cannot find/get file in Drive: {str(file_error)}") 195 | drive_web_view_link = None # Ensure it's None if error occurs 196 | 197 | # Set public permission 198 | permission = { 199 | 'type': 'anyone', 200 | 'role': 'reader', 201 | 'allowFileDiscovery': True 202 | } 203 | perm_result = self.drive_service.permissions().create( 204 | fileId=form_id, 205 | body=permission, 206 | fields='id', 207 | sendNotificationEmail=False 208 | ).execute() 209 | print(f"DEBUG: Permissions set successfully: {perm_result}") 210 | print(f"DEBUG: Full permissions create response: {json.dumps(perm_result, indent=2)}") # Log full response 211 | 212 | # Check permissions after setting 213 | permissions = self.drive_service.permissions().list( 214 | fileId=form_id, 215 | fields="*" # Get all fields 216 | ).execute() 217 | print(f"DEBUG: Full permissions list response after setting: {json.dumps(permissions, indent=2)}") # Log full response 218 | 219 | # Try to publish the file using the Drive API - This might be unnecessary/problematic 220 | try: 221 | publish_body = { 222 | 'published': True, 223 | 'publishedOutsideDomain': True, 224 | 'publishAuto': True 225 | } 226 | self.drive_service.revisions().update( 227 | fileId=form_id, 228 | revisionId='head', 229 | body=publish_body 230 | ).execute() 231 | print("DEBUG: Form published successfully via Drive API") 232 | except Exception as publish_error: 233 | print(f"DEBUG: Non-critical publish error: {str(publish_error)}") 234 | except Exception as perm_error: 235 | print(f"DEBUG: Permission error: {str(perm_error)}") 236 | # Continue even if permission setting fails 237 | 238 | # Determine the final response_url based on availability and priority 239 | # Priority: responderUri from settings, initial responderUri, Drive webViewLink (less reliable for view), fallback 240 | response_url = None 241 | if settings_responder_uri: 242 | response_url = settings_responder_uri 243 | print(f"DEBUG: FINAL URL: Using responderUri from settings response: {response_url}") 244 | elif initial_responder_uri: 245 | response_url = initial_responder_uri 246 | print(f"DEBUG: FINAL URL: Using initial responderUri from create response: {response_url}") 247 | elif drive_web_view_link and "/viewform" in drive_web_view_link: # Only use webViewLink if it looks like a view link 248 | response_url = drive_web_view_link 249 | print(f"DEBUG: FINAL URL: Using Drive webViewLink (as it contained /viewform): {response_url}") 250 | else: 251 | # Fallback to manual construction if absolutely nothing else is found 252 | response_url = f"https://docs.google.com/forms/d/e/{form_id}/viewform" # Use /e/ format for fallback 253 | print(f"DEBUG: FINAL URL: Using manually constructed fallback (/e/ format): {response_url}") 254 | # Also log the potentially problematic webViewLink if it existed but wasn't used 255 | if drive_web_view_link: 256 | print(f"DEBUG: Note: Drive webViewLink found but not used as final URL: {drive_web_view_link}") 257 | 258 | # Ensure edit_url is correctly set (it's usually stable) 259 | edit_url = f"https://docs.google.com/forms/d/{form_id}/edit" 260 | print(f"DEBUG: FINAL Edit URL: {edit_url}") 261 | 262 | return { 263 | "form_id": form_id, 264 | "response_url": response_url, 265 | "edit_url": edit_url, 266 | "title": title 267 | } 268 | except Exception as e: 269 | print(f"Error creating form: {str(e)}") 270 | raise 271 | 272 | def add_question(self, form_id, question_type, title, options=None, required=False): 273 | """ 274 | Add a question to an existing Google Form. 275 | 276 | Args: 277 | form_id: ID of the form to add the question to 278 | question_type: Type of question (text, paragraph, multiple_choice, etc.) 279 | title: Question title/text 280 | options: List of options for multiple choice questions 281 | required: Whether the question is required 282 | 283 | Returns: 284 | dict: Response containing question ID 285 | """ 286 | try: 287 | print(f"DEBUG: Adding {question_type} question to form {form_id}") 288 | # Get the current form 289 | form = self.forms_service.forms().get(formId=form_id).execute() 290 | 291 | # Determine the item ID for the new question 292 | item_id = len(form.get('items', [])) 293 | print(f"DEBUG: New question will have item_id: {item_id}") 294 | 295 | # Create base request 296 | request = { 297 | "requests": [{ 298 | "createItem": { 299 | "item": { 300 | "title": title, 301 | "questionItem": { 302 | "question": { 303 | "required": required 304 | } 305 | } 306 | }, 307 | "location": { 308 | "index": item_id 309 | } 310 | } 311 | }] 312 | } 313 | 314 | # Set up question type specific configuration 315 | if question_type == "text": 316 | print("DEBUG: Setting up text question") 317 | request["requests"][0]["createItem"]["item"]["questionItem"]["question"]["textQuestion"] = {} 318 | 319 | elif question_type == "paragraph": 320 | print("DEBUG: Setting up paragraph question") 321 | # Google Forms API uses textQuestion with different properties for paragraphs 322 | request["requests"][0]["createItem"]["item"]["questionItem"]["question"]["textQuestion"] = { 323 | "paragraph": True 324 | } 325 | 326 | elif question_type == "multiple_choice" and options: 327 | print(f"DEBUG: Setting up multiple choice question with {len(options)} options") 328 | choices = [{"value": option} for option in options] 329 | request["requests"][0]["createItem"]["item"]["questionItem"]["question"]["choiceQuestion"] = { 330 | "type": "RADIO", 331 | "options": choices, 332 | "shuffle": False 333 | } 334 | 335 | elif question_type == "checkbox" and options: 336 | print(f"DEBUG: Setting up checkbox question with {len(options)} options") 337 | choices = [{"value": option} for option in options] 338 | request["requests"][0]["createItem"]["item"]["questionItem"]["question"]["choiceQuestion"] = { 339 | "type": "CHECKBOX", 340 | "options": choices, 341 | "shuffle": False 342 | } 343 | 344 | print(f"DEBUG: Request body: {request}") 345 | 346 | # Execute the request 347 | update_response = self.forms_service.forms().batchUpdate( 348 | formId=form_id, 349 | body=request 350 | ).execute() 351 | print(f"DEBUG: Question added successfully: {update_response}") 352 | 353 | return { 354 | "form_id": form_id, 355 | "question_id": item_id, 356 | "title": title, 357 | "type": question_type 358 | } 359 | except Exception as e: 360 | print(f"Error adding question: {str(e)}") 361 | raise 362 | 363 | def get_responses(self, form_id): 364 | """ 365 | Get responses for a Google Form. 366 | 367 | Args: 368 | form_id: ID of the form to get responses for 369 | 370 | Returns: 371 | dict: Form responses 372 | """ 373 | try: 374 | # Get the form to retrieve question titles 375 | form = self.forms_service.forms().get(formId=form_id).execute() 376 | questions = {} 377 | 378 | for item in form.get('items', []): 379 | question_id = item.get('itemId', '') 380 | title = item.get('title', '') 381 | questions[question_id] = title 382 | 383 | # Get form responses 384 | response_data = self.forms_service.forms().responses().list(formId=form_id).execute() 385 | responses = response_data.get('responses', []) 386 | 387 | formatted_responses = [] 388 | for response in responses: 389 | answer_data = {} 390 | answers = response.get('answers', {}) 391 | 392 | for question_id, answer in answers.items(): 393 | question_title = questions.get(question_id, question_id) 394 | 395 | if 'textAnswers' in answer: 396 | text_values = [text.get('value', '') for text in answer.get('textAnswers', {}).get('answers', [])] 397 | answer_data[question_title] = text_values[0] if len(text_values) == 1 else text_values 398 | 399 | elif 'choiceAnswers' in answer: 400 | choice_values = answer.get('choiceAnswers', {}).get('answers', []) 401 | answer_data[question_title] = choice_values 402 | 403 | formatted_responses.append({ 404 | 'response_id': response.get('responseId', ''), 405 | 'created_time': response.get('createTime', ''), 406 | 'answers': answer_data 407 | }) 408 | 409 | return { 410 | "form_id": form_id, 411 | "form_title": form.get('info', {}).get('title', ''), 412 | "response_count": len(formatted_responses), 413 | "responses": formatted_responses 414 | } 415 | except Exception as e: 416 | print(f"Error getting responses: {str(e)}") 417 | raise 418 | ``` -------------------------------------------------------------------------------- /agents/agent_integration.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | CamelAIOrg Agent Integration for Google Forms MCP Server 3 | 4 | This module provides integration with CamelAIOrg's agent framework, 5 | enabling natural language processing to create Google Forms through MCP. 6 | """ 7 | 8 | import os 9 | import json 10 | import logging 11 | import requests 12 | import datetime 13 | 14 | # REMOVE: Import our mock CamelAI implementation 15 | # from camelai import create_agent 16 | 17 | # Load environment variables 18 | # load_dotenv() 19 | 20 | # Configure logging 21 | logging.basicConfig( 22 | level=logging.INFO, 23 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 24 | ) 25 | logger = logging.getLogger("agent_integration") 26 | 27 | # Configuration 28 | MCP_SERVER_URL = os.getenv('MCP_SERVER_URL', 'http://mcp-server:5000/api/process') 29 | AGENT_API_KEY = os.getenv('AGENT_API_KEY', 'demo_key') # Might be used for a real LLM API key 30 | 31 | # Import Camel AI components (assuming structure) 32 | from camel.agents import ChatAgent 33 | from camel.messages import BaseMessage 34 | from camel.types import ModelPlatformType, ModelType, RoleType # Added RoleType 35 | from camel.models import ModelFactory 36 | 37 | class FormAgent: 38 | """ 39 | Agent for handling natural language form creation requests. 40 | Uses NLP (simulated via _call_llm_agent) to process natural language 41 | and convert it to MCP tool calls. 42 | """ 43 | 44 | def __init__(self): 45 | """Initialize the agent.""" 46 | self.logger = logger 47 | self.log_entries = [] # Initialize log storage 48 | # REMOVE: self.camel_agent = create_agent("FormAgent", ...) 49 | self.logger.info("FormAgent initialized (using simulated LLM)") 50 | 51 | def _log_step(self, step_type, data): 52 | """Adds a structured log entry for the frontend.""" 53 | entry = { 54 | "timestamp": datetime.datetime.now().isoformat(), 55 | "step_type": step_type, 56 | "data": data 57 | } 58 | self.log_entries.append(entry) 59 | # Also log to server console for debugging 60 | self.logger.info(f"AGENT LOG STEP: {step_type} - {data}") 61 | 62 | def process_request(self, request_text): 63 | """ 64 | Processes a natural language request using a simulated LLM call. 65 | 1. Calls a simulated LLM to parse the request into structured JSON. 66 | 2. Determines the necessary MCP tool calls based on the JSON. 67 | 3. Executes the tool calls by sending requests to the MCP server. 68 | 4. Orchestrates multi-step processes. 69 | 5. Returns the final result or error. 70 | """ 71 | self.log_entries = [] # Clear log for new request 72 | self._log_step("Request Received", {"text": request_text}) 73 | 74 | try: 75 | # 1. Analyze request using simulated LLM call 76 | self._log_step("NLP Analysis Start", {}) 77 | structured_form_data = self._call_llm_agent(request_text) 78 | self.logger.info(f"Simulated LLM Structured Output: {json.dumps(structured_form_data, indent=2)}") 79 | self._log_step("NLP Analysis Complete", {"structured_data": structured_form_data}) 80 | 81 | # Basic validation of LLM output 82 | if not structured_form_data or 'formTitle' not in structured_form_data: 83 | self.logger.error("Simulated LLM did not return valid structured data.") 84 | return { "status": "error", "message": "Failed to understand the request structure using LLM." } 85 | 86 | # Extract sections and settings - Basic structure assumes one form for now 87 | form_params = { 88 | "title": structured_form_data.get('formTitle'), 89 | "description": structured_form_data.get('formDescription', '') 90 | # We would also handle settings here if the API supported it 91 | } 92 | sections = structured_form_data.get('sections', []) 93 | if not sections: 94 | self.logger.error("Simulated LLM did not return any form sections/questions.") 95 | return { "status": "error", "message": "LLM did not identify any questions for the form." } 96 | 97 | # 2. Determine and execute tool calls (simplified flow: create form, add all questions) 98 | # NOTE: This doesn't handle multiple sections, logic, or advanced settings yet 99 | all_questions = [] 100 | for section in sections: 101 | # TODO: Add support for creating sections/page breaks if API allows 102 | # self.logger.info(f"Processing section: {section.get('title', 'Untitled Section')}") 103 | all_questions.extend(section.get('questions', [])) 104 | 105 | # Pass the extracted questions to the creation flow 106 | form_params['questions'] = all_questions 107 | # Execute the flow, which will populate self.log_entries further 108 | final_response = self._execute_create_form_flow(form_params) 109 | 110 | # Add the collected logs to the final response if successful 111 | if final_response.get("status") == "success": 112 | final_response["log_entries"] = self.log_entries 113 | 114 | return final_response 115 | 116 | except Exception as e: 117 | self.logger.error(f"Error in FormAgent process_request: {str(e)}", exc_info=True) 118 | self._log_step("Agent Error", {"error": str(e)}) 119 | # Include logs gathered so far in the error response too 120 | return { 121 | "status": "error", 122 | "message": f"Agent failed to process request: {str(e)}", 123 | "log_entries": self.log_entries 124 | } 125 | 126 | def _call_llm_agent(self, request_text): 127 | """ 128 | Uses Camel AI's ChatAgent to process the request and extract structured data. 129 | Falls back to a basic structure if the LLM call fails. 130 | """ 131 | # Check for the API key first (using the name Camel AI expects) 132 | api_key = os.getenv('GOOGLE_API_KEY') 133 | if not api_key: 134 | self.logger.error("GOOGLE_API_KEY is not set in the environment.") # Updated log message 135 | return self._get_fallback_structure(request_text) 136 | 137 | try: 138 | # 1. Setup Model 139 | # Assuming ModelFactory can find the key from env or we pass it 140 | # Adjust ModelType enum based on actual Camel AI definition if needed 141 | # Using a placeholder like ModelType.GEMINI_1_5_FLASH - this will likely need correction 142 | model_name_str = "gemini-1.5-flash-latest" # Keep the string name 143 | # Attempt to create model instance via factory 144 | # Create the required config dict - make it empty and rely on env var GOOGLE_API_KEY 145 | model_config_dict = { 146 | # No API key here - expecting library to read GOOGLE_API_KEY from env 147 | } 148 | # REVERT: Go back to GEMINI platform type 149 | llm_model = ModelFactory.create( 150 | model_platform=ModelPlatformType.GEMINI, 151 | model_type=ModelType.GEMINI_1_5_FLASH, 152 | model_config_dict=model_config_dict 153 | ) 154 | self.logger.info(f"Camel AI Model configured using: {ModelType.GEMINI_1_5_FLASH}") 155 | 156 | # 2. Prepare messages for the ChatAgent 157 | system_prompt = self._build_llm_prompt(request_text) 158 | system_message = BaseMessage( 159 | role_name="System", 160 | role_type=RoleType.ASSISTANT, 161 | meta_dict=None, 162 | content=system_prompt 163 | ) 164 | user_message = BaseMessage( 165 | role_name="User", 166 | role_type=RoleType.USER, 167 | meta_dict=None, 168 | content=request_text # Or maybe just a trigger like "Process the request"? Let's try request_text 169 | ) 170 | 171 | # 3. Initialize and run the ChatAgent 172 | agent = ChatAgent(system_message=system_message, model=llm_model) 173 | agent.reset() # Ensure clean state 174 | 175 | self.logger.info("Calling Camel AI ChatAgent...") 176 | response = agent.step(user_message) 177 | 178 | if not response or not response.msgs: 179 | self.logger.error("Camel AI agent did not return a valid response.") 180 | return self._get_fallback_structure(request_text) 181 | 182 | # 4. Extract JSON content from the last message 183 | # Assuming the response structure contains a list of messages `msgs` 184 | # and the agent's reply is the last one. 185 | agent_reply_message = response.msgs[-1] 186 | content = agent_reply_message.content 187 | self.logger.debug(f"Raw Camel AI agent response content: {content}") 188 | 189 | # --- Robust JSON Extraction --- 190 | try: 191 | # Find the start and end of the JSON object 192 | json_start = content.find('{') 193 | json_end = content.rfind('}') 194 | 195 | if json_start != -1 and json_end != -1 and json_end > json_start: 196 | json_string = content[json_start:json_end+1] 197 | structured_data = json.loads(json_string) 198 | self.logger.info("Successfully parsed structured JSON from Camel AI agent response.") 199 | return structured_data 200 | else: 201 | # Fallback if JSON bounds couldn't be found 202 | self.logger.error(f"Could not find valid JSON object boundaries in response. Content: {content}") 203 | return self._get_fallback_structure(request_text) 204 | 205 | except json.JSONDecodeError as e: 206 | self.logger.error(f"Failed to parse JSON content from Camel AI agent: {e}. Extracted content attempt: {json_string if 'json_string' in locals() else 'N/A'}") 207 | return self._get_fallback_structure(request_text) 208 | 209 | except ImportError as e: 210 | self.logger.error(f"Failed to import Camel AI components. Is 'camel-ai' installed correctly? Error: {e}") 211 | return self._get_fallback_structure(request_text) 212 | except Exception as e: 213 | # Catch potential errors during Camel AI model init or agent step 214 | self.logger.error(f"Error during Camel AI processing: {str(e)}", exc_info=True) 215 | return self._get_fallback_structure(request_text) 216 | 217 | def _build_llm_prompt(self, request_text): 218 | """ 219 | Constructs the detailed prompt for the LLM. 220 | 221 | Crucial for getting reliable JSON output. 222 | Needs careful tuning based on the LLM used. 223 | """ 224 | # Define the desired JSON structure and supported types/features 225 | # This helps the LLM understand the target format. 226 | json_schema_description = """ 227 | { 228 | "formTitle": "string", 229 | "formDescription": "string (optional)", 230 | "settings": { 231 | // Note: Backend does not support settings yet, but LLM can parse them 232 | "collectEmail": "string (optional: 'required' or 'optional')", 233 | "limitToOneResponse": "boolean (optional)", 234 | "progressBar": "boolean (optional)", 235 | "confirmationMessage": "string (optional)" 236 | }, 237 | "sections": [ 238 | { 239 | "title": "string", 240 | "description": "string (optional)", 241 | "questions": [ 242 | { 243 | "title": "string", 244 | "description": "string (optional)", 245 | "type": "string (enum: text, paragraph, multiple_choice, checkbox, linear_scale, multiple_choice_grid, checkbox_grid)", 246 | "required": "boolean (optional, default: false)", 247 | "options": "array of strings (required for multiple_choice, checkbox)" 248 | + " OR object { min: int, max: int, minLabel: string (opt), maxLabel: string (opt) } (required for linear_scale)" 249 | + " OR object { rows: array of strings, columns: array of strings } (required for grids)", 250 | "logic": "object { on: string (option value), action: string (enum: submit_form, skip_section), targetSectionTitle: string (required if action=skip_section) } (optional)", // Note: Backend doesn't support logic 251 | "validation": "string (optional: 'email' or other types if supported)" // Note: Backend doesn't support validation 252 | } 253 | // ... more questions ... 254 | ] 255 | } 256 | // ... more sections ... 257 | ] 258 | } 259 | """ 260 | 261 | prompt = f"""Analyze the following user request to create a Google Form. Based ONLY on the request, generate a JSON object strictly adhering to the following schema. 262 | 263 | SCHEMA: 264 | ```json 265 | {json_schema_description} 266 | ``` 267 | 268 | RULES: 269 | - Output *only* the JSON object, nothing else before or after. 270 | - If the user requests features not supported by the schema description (e.g., file uploads, specific themes, complex scoring), omit them from the JSON. 271 | - If the request is unclear about question types or options, make reasonable assumptions (e.g., use 'text' for simple questions, provide standard options for ratings). 272 | - Ensure the 'options' field matches the required format for the specified 'type'. 273 | - Pay close attention to required fields in the schema description. 274 | - If the request seems too simple or doesn't clearly describe a form, create a basic form structure with a title based on the request and a few generic questions (like Name, Email, Comments). 275 | 276 | USER REQUEST: 277 | ``` 278 | {request_text} 279 | ``` 280 | 281 | JSON OUTPUT: 282 | """ 283 | return prompt 284 | 285 | def _get_fallback_structure(self, request_text): 286 | """Returns a basic structure if LLM call fails or parsing fails.""" 287 | self.logger.warning(f"Gemini call/parsing failed. Falling back to basic structure for: {request_text}") 288 | return { 289 | "formTitle": self._generate_title(request_text), 290 | "formDescription": request_text, 291 | "settings": {}, 292 | "sections": [ 293 | { 294 | "questions": [ 295 | {"title": "Name", "type": "text", "required": True}, 296 | {"title": "Email", "type": "text", "required": False}, 297 | {"title": "Your Response", "type": "paragraph", "required": True} 298 | ] 299 | } 300 | ] 301 | } 302 | 303 | def _execute_create_form_flow(self, params): 304 | """ 305 | Handles the complete flow for creating a form and adding its questions. 306 | """ 307 | self.logger.info(f"Executing create form flow with params: {params}") 308 | 309 | # Extract potential questions from NLP parameters 310 | questions_to_add = params.pop('questions', []) # Remove questions from main params 311 | 312 | # A. Create the form first 313 | self._log_step("Create Form Start", {"params": params}) 314 | create_form_response = self._handle_create_form(params) 315 | self._log_step("Create Form End", {"response": create_form_response}) 316 | 317 | if create_form_response.get("status") != "success": 318 | self.logger.error(f"Form creation failed: {create_form_response.get('message')}") 319 | return create_form_response # Return the error from create_form 320 | 321 | # Get the form_id from the successful response 322 | form_result = create_form_response.get('result', {}) 323 | form_id = form_result.get('form_id') 324 | 325 | if not form_id: 326 | self.logger.error("Form creation succeeded but no form_id returned.") 327 | return { "status": "error", "message": "Form created, but form_id missing in response." } 328 | 329 | self.logger.info(f"Form created successfully: {form_id}") 330 | 331 | # B. Add questions if any were identified by NLP 332 | question_results = [] 333 | if questions_to_add: 334 | self._log_step("Add Questions Start", {"count": len(questions_to_add)}) 335 | self.logger.info(f"Adding {len(questions_to_add)} questions to form {form_id}") 336 | for question_data in questions_to_add: 337 | question_params = { 338 | "form_id": form_id, 339 | # Ensure structure matches _handle_add_question needs 340 | "type": question_data.get('type'), 341 | "title": question_data.get('title'), 342 | "options": question_data.get('options', []), 343 | "required": question_data.get('required', False) 344 | } 345 | 346 | # Validate basic question params before sending 347 | if not question_params['type'] or not question_params['title']: 348 | self.logger.warning(f"Skipping invalid question data: {question_data}") 349 | continue 350 | 351 | self._log_step("Add Question Start", {"question_index": questions_to_add.index(question_data), "params": question_params}) 352 | add_q_response = self._handle_add_question(question_params) 353 | self._log_step("Add Question End", {"question_index": questions_to_add.index(question_data), "response": add_q_response}) 354 | question_results.append(add_q_response) 355 | 356 | # Log if the question type wasn't supported by the backend 357 | if add_q_response.get("status") == "error" and "Invalid question_type" in add_q_response.get("message", ""): 358 | self.logger.warning(f"Question '{question_params['title']}' failed: Type '{question_params['type']}' likely not supported by backend API yet.") 359 | elif add_q_response.get("status") != "success": 360 | self.logger.error(f"Failed to add question '{question_params['title']}': {add_q_response.get('message')}") 361 | 362 | # Optional: Check if question adding failed and decide whether to stop 363 | if add_q_response.get("status") != "success": 364 | self.logger.error(f"Failed to add question '{question_params['title']}': {add_q_response.get('message')}") 365 | # Decide: continue adding others, or return error immediately? 366 | # For now, let's continue but log the error. 367 | self._log_step("Add Questions End", {}) 368 | else: 369 | self.logger.info(f"No questions identified by NLP to add to form {form_id}") 370 | self._log_step("Add Questions Skipped", {}) 371 | 372 | # 5. Return the final result (details of the created form) 373 | # We can optionally include the results of adding questions if needed 374 | final_result = form_result 375 | # Let's add the questions that were *attempted* to be added back for the UI 376 | final_result['questions'] = questions_to_add 377 | 378 | return { 379 | "status": "success", 380 | "result": final_result 381 | # Optionally add: "question_addition_results": question_results 382 | } 383 | 384 | # Add the fallback title generation method back (or use a simpler one) 385 | def _generate_title(self, request_text): 386 | words = request_text.split() 387 | if len(words) <= 5: 388 | return request_text + " Form" 389 | else: 390 | return " ".join(words[:5]) + "... Form" 391 | 392 | # --- Methods for handling specific tool calls by sending to MCP Server --- 393 | 394 | def _handle_create_form(self, params): 395 | """ 396 | Handle form creation by sending MCP packet to the server. 397 | 398 | Args: 399 | params: Parameters for form creation 400 | 401 | Returns: 402 | dict: Result of the form creation 403 | """ 404 | self.logger.info(f"Creating form with params: {params}") 405 | 406 | # Prepare MCP packet 407 | mcp_packet = { 408 | "tool_name": "create_form", 409 | "parameters": { 410 | "title": params.get("title", "Form from NL Request"), 411 | "description": params.get("description", "") 412 | } 413 | } 414 | 415 | # Log *before* sending 416 | self._log_step("MCP Request (create_form)", mcp_packet) 417 | response = self._send_to_mcp_server(mcp_packet) 418 | # Log *after* receiving 419 | self._log_step("MCP Response (create_form)", response) 420 | return response 421 | 422 | def _handle_add_question(self, params): 423 | """ 424 | Handle adding a question to a form by sending MCP packet. 425 | 426 | Args: 427 | params: Parameters for question addition 428 | 429 | Returns: 430 | dict: Result of the question addition 431 | """ 432 | self.logger.info(f"Adding question with params: {params}") 433 | 434 | # Prepare MCP packet 435 | mcp_packet = { 436 | "tool_name": "add_question", 437 | "parameters": { 438 | "form_id": params.get("form_id"), 439 | "question_type": params.get("type", "text"), 440 | "title": params.get("title", "Question"), 441 | "options": params.get("options", []), 442 | "required": params.get("required", False) 443 | } 444 | } 445 | 446 | # Log *before* sending 447 | self._log_step("MCP Request (add_question)", mcp_packet) 448 | response = self._send_to_mcp_server(mcp_packet) 449 | # Log *after* receiving 450 | self._log_step("MCP Response (add_question)", response) 451 | return response 452 | 453 | def _handle_get_responses(self, params): 454 | """ 455 | Handle getting form responses by sending MCP packet. 456 | 457 | Args: 458 | params: Parameters for getting responses 459 | 460 | Returns: 461 | dict: Form responses 462 | """ 463 | self.logger.info(f"Getting responses with params: {params}") 464 | 465 | # Prepare MCP packet 466 | mcp_packet = { 467 | "tool_name": "get_responses", 468 | "parameters": { 469 | "form_id": params.get("form_id") 470 | } 471 | } 472 | 473 | # Log *before* sending 474 | self._log_step("MCP Request (get_responses)", mcp_packet) 475 | response = self._send_to_mcp_server(mcp_packet) 476 | # Log *after* receiving 477 | self._log_step("MCP Response (get_responses)", response) 478 | return response 479 | 480 | def _send_to_mcp_server(self, mcp_packet): 481 | """ 482 | Sends an MCP packet to the MCP server URL. 483 | 484 | Args: 485 | mcp_packet: The MCP packet (dict) to send. 486 | 487 | Returns: 488 | dict: The response JSON from the MCP server. 489 | """ 490 | self.logger.info(f"Sending MCP packet: {json.dumps(mcp_packet)}") 491 | 492 | try: 493 | response = requests.post( 494 | MCP_SERVER_URL, 495 | json=mcp_packet, 496 | headers={'Content-Type': 'application/json'}, 497 | timeout=30 # Add a timeout (e.g., 30 seconds) 498 | ) 499 | 500 | # Raise an exception for bad status codes (4xx or 5xx) 501 | response.raise_for_status() 502 | 503 | response_data = response.json() 504 | self.logger.info(f"Received MCP response: {json.dumps(response_data)}") 505 | return response_data 506 | 507 | except requests.exceptions.Timeout: 508 | self.logger.error(f"Timeout sending MCP packet to {MCP_SERVER_URL}") 509 | return {"status": "error", "message": "MCP server request timed out"} 510 | except requests.exceptions.RequestException as e: 511 | self.logger.error(f"Error sending MCP packet to {MCP_SERVER_URL}: {str(e)}") 512 | # Try to get error details from response body if possible 513 | error_detail = str(e) 514 | try: 515 | error_detail = e.response.text if e.response else str(e) 516 | except Exception: 517 | pass # Ignore errors parsing the error response itself 518 | return {"status": "error", "message": f"MCP server communication error: {error_detail}"} 519 | except json.JSONDecodeError as e: 520 | self.logger.error(f"Error decoding MCP response JSON: {str(e)}") 521 | return {"status": "error", "message": "Invalid JSON response from MCP server"} 522 | except Exception as e: 523 | self.logger.error(f"Unexpected error in _send_to_mcp_server: {str(e)}", exc_info=True) 524 | return {"status": "error", "message": f"Unexpected agent error sending MCP packet: {str(e)}"} 525 | 526 | 527 | # For testing 528 | if __name__ == "__main__": 529 | agent = FormAgent() 530 | 531 | # Test with some sample requests 532 | test_requests = [ 533 | "Create a customer feedback form with a rating question", 534 | "Make a survey about remote work preferences", 535 | "Set up an RSVP form for my event on Saturday" 536 | ] 537 | 538 | for req in test_requests: 539 | print(f"\nProcessing: {req}") 540 | result = agent.process_request(req) 541 | print(f"Result: {json.dumps(result, indent=2)}") 542 | ```