#
tokens: 41374/50000 30/30 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | 
```