添加 http_handler.py

This commit is contained in:
2025-04-16 19:56:25 +08:00
parent d145b6a83b
commit 13796de9cd

399
http_handler.py Normal file
View File

@@ -0,0 +1,399 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Maya MCP HTTP Handler
This module provides HTTP request handling for the Maya MCP server.
Version: 1.0.0
Author: Jeffrey Tsai
"""
import os
import sys
import json
import time
import traceback
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from port_config import SERVER_HOST, SERVER_PORT
from log_config import get_logger, initialize_logging
# Initialize logging
initialize_logging()
logger = get_logger("HTTPHandler")
# Global variables
_server_instance = None
_server_running = False
_clients = []
_event_callbacks = {}
class MCPHTTPHandler(BaseHTTPRequestHandler):
"""HTTP request handler for Maya MCP server"""
def do_GET(self):
"""Handle GET requests"""
try:
# Check if this is an SSE request
accept_header = self.headers.get('Accept', '')
logger.debug(f"Received GET request for path: {self.path}")
logger.debug(f"Headers: {self.headers}")
# Support both root path and /events path for SSE
if ('text/event-stream' in accept_header and
(self.path == '/' or self.path == '/events')):
logger.info(f"Handling SSE request from {self.client_address[0]}:{self.client_address[1]}")
self._handle_sse_request()
else:
logger.info(f"Handling regular request for path: {self.path}")
self._handle_regular_request()
except Exception as e:
logger.error(f"Error handling GET request: {e}")
logger.error(traceback.format_exc())
self._send_error(500, str(e))
def _handle_sse_request(self):
"""Handle Server-Sent Events (SSE) request"""
try:
# Add client to list
client_id = f"client-{int(time.time())}"
_clients.append((self, client_id))
logger.info(f"New SSE client connected: {client_id}")
# Send SSE headers
self.send_response(200)
self.send_header('Content-Type', 'text/event-stream')
self.send_header('Cache-Control', 'no-cache')
self.send_header('Connection', 'keep-alive')
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
self.send_header('X-Accel-Buffering', 'no') # Disable Nginx buffering
self.end_headers()
# Send initial comment to keep connection alive
self.wfile.write(": ping\n\n".encode('utf-8'))
self.wfile.flush()
logger.debug(f"Sent initial ping to client {client_id}")
# Send connection event
connection_data = {
"status": "connected",
"client_id": client_id,
"server_port": SERVER_PORT,
"server_type": "maya",
"version": "1.0.0",
"timestamp": int(time.time() * 1000),
"protocol": "SSE"
}
self._send_sse_event("connection", connection_data)
# Send ready event
ready_data = {
"status": "ready",
"client_id": client_id,
"timestamp": int(time.time() * 1000)
}
self._send_sse_event("ready", ready_data)
# Keep connection alive with heartbeats
heartbeat_count = 0
while True:
try:
# Send heartbeat every 5 seconds
time.sleep(5)
heartbeat_data = {
"timestamp": int(time.time() * 1000),
"client_id": client_id
}
self._send_sse_event("heartbeat", heartbeat_data)
heartbeat_count += 1
if heartbeat_count % 12 == 0: # Log every minute (12 * 5 seconds)
logger.debug(f"Sent {heartbeat_count} heartbeats to client {client_id}")
except Exception as e:
logger.error(f"Error in SSE loop: {e}")
logger.debug(traceback.format_exc())
break
except Exception as e:
logger.error(f"Error handling SSE request: {e}")
logger.error(traceback.format_exc())
finally:
# Remove client from list
if (self, client_id) in _clients:
_clients.remove((self, client_id))
logger.info(f"SSE client disconnected: {client_id}")
def _send_sse_event(self, event_type, data):
"""Send SSE event"""
try:
# Convert data to JSON
data_json = json.dumps(data)
# Create SSE event
event = f"event: {event_type}\n"
event += f"data: {data_json}\n\n"
# Send event
self.wfile.write(event.encode('utf-8'))
self.wfile.flush()
logger.debug(f"Sent SSE event: {event_type}")
except Exception as e:
logger.error(f"Error sending SSE event: {e}")
logger.error(traceback.format_exc())
raise
def _handle_regular_request(self):
"""Handle regular HTTP request"""
try:
# Default response
status_code = 200
content_type = "application/json"
response_data = {"status": "ok"}
# Handle different paths
if self.path == "/" or self.path == "/info":
# Get server info
response_data = {
"name": "Maya MCP Server",
"version": "1.0.0",
"port": SERVER_PORT
}
logger.debug(f"Sending server info: {response_data}")
elif self.path == "/scene":
# Get scene info
try:
# First try to import the function
try:
from server import get_scene_info
logger.info("Successfully imported get_scene_info function")
except ImportError as ie:
logger.error(f"Error importing get_scene_info: {ie}")
logger.error(traceback.format_exc())
status_code = 500
response_data = {"error": "Failed to import scene info function", "details": str(ie)}
self._send_response(status_code, content_type, response_data)
return
# Then try to call the function with a timeout
try:
logger.info("Attempting to get scene info...")
response_data = get_scene_info()
logger.info(f"Scene info retrieved successfully: {len(str(response_data))} bytes")
except Exception as e:
logger.error(f"Error calling get_scene_info: {e}")
logger.error(traceback.format_exc())
status_code = 500
response_data = {
"error": "Failed to get scene info",
"details": str(e),
"fallback_info": {
"status": "error",
"message": "Could not retrieve scene information",
"timestamp": int(time.time() * 1000)
}
}
except Exception as outer_e:
logger.error(f"Outer exception in scene info handler: {outer_e}")
logger.error(traceback.format_exc())
status_code = 500
response_data = {"error": "Internal server error", "details": str(outer_e)}
else:
# Unknown path
status_code = 404
response_data = {"error": f"Path not found: {self.path}"}
logger.warning(f"Unknown path requested: {self.path}")
# Send response
self._send_response(status_code, content_type, response_data)
except Exception as e:
logger.error(f"Error handling regular request: {e}")
logger.error(traceback.format_exc())
self._send_error(500, str(e))
def _send_response(self, status_code, content_type, data):
"""Send HTTP response"""
try:
# Send response
self.send_response(status_code)
self.send_header('Content-Type', content_type)
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
# Send data
response_json = json.dumps(data)
self.wfile.write(response_json.encode('utf-8'))
logger.debug(f"Sent response with status {status_code}, size: {len(response_json)} bytes")
except Exception as e:
logger.error(f"Error sending response: {e}")
logger.error(traceback.format_exc())
raise
def _send_error(self, status_code, error_message):
"""Send error response"""
try:
# Send error response
self.send_response(status_code)
self.send_header('Content-Type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
# Send error data
error_data = {"error": error_message}
error_json = json.dumps(error_data)
self.wfile.write(error_json.encode('utf-8'))
logger.debug(f"Sent error response with status {status_code}: {error_message}")
except Exception as e:
logger.error(f"Error sending error response: {e}")
logger.error(traceback.format_exc())
# Don't raise here to avoid infinite recursion
def log_message(self, format, *args):
"""Override log_message to use our logger"""
logger.debug(f"{self.address_string()} - {format % args}")
# Server class
class MCPHTTPServer:
"""HTTP server for Maya MCP"""
def __init__(self, port=SERVER_PORT):
"""Initialize server"""
self.port = port
self.server = None
self.thread = None
self.running = False
def start(self):
"""Start server"""
try:
# Create server
print(f"Attempting to start HTTP server on {SERVER_HOST}:{self.port}")
self.server = HTTPServer((SERVER_HOST, self.port), MCPHTTPHandler)
# Get actual port used
_, self.port = self.server.server_address
self.running = True
# Run server in a separate thread
self.thread = threading.Thread(target=self._run_server)
self.thread.daemon = True
self.thread.start()
print(f"HTTP server started successfully on {SERVER_HOST}:{self.port}")
logger.info(f"HTTP server started on {SERVER_HOST}:{self.port}")
return True
except Exception as e:
print(f"Error starting HTTP server: {e}")
logger.error(f"Error starting HTTP server: {e}")
logger.error(traceback.format_exc())
self.stop()
return False
def stop(self):
"""Stop server"""
if not self.running:
return False
self.running = False
# Stop server
if self.server:
try:
self.server.shutdown()
self.server.server_close()
except:
pass
self.server = None
logger.info("HTTP server stopped")
return True
def is_running(self):
"""Check if server is running"""
return self.running
def _run_server(self):
"""Server main loop"""
logger.info("HTTP server main loop started")
try:
self.server.serve_forever()
except Exception as e:
if self.running:
logger.error(f"Error in HTTP server loop: {e}")
logger.error(traceback.format_exc())
logger.info("HTTP server main loop ended")
# Public functions
def start_http_server(port=SERVER_PORT):
"""Start HTTP server"""
global _server_instance, SERVER_PORT, _server_running
try:
# Check if server is already running
if _server_running and _server_instance:
logger.info(f"HTTP server already running on port {SERVER_PORT}")
return SERVER_PORT
# Create and start server
SERVER_PORT = port
_server_instance = MCPHTTPServer(port)
if _server_instance.start():
_server_running = True
SERVER_PORT = _server_instance.port
return SERVER_PORT
else:
_server_instance = None
return None
except Exception as e:
logger.error(f"Error starting HTTP server: {e}")
logger.error(traceback.format_exc())
return None
def stop_http_server():
"""Stop HTTP server"""
global _server_instance, _server_running
try:
if _server_instance and _server_running:
success = _server_instance.stop()
if success:
_server_running = False
_server_instance = None
return success
return False
except Exception as e:
logger.error(f"Error stopping HTTP server: {e}")
logger.error(traceback.format_exc())
return False
def is_http_server_running():
"""Check if HTTP server is running"""
global _server_instance, _server_running
if _server_instance and _server_running:
return _server_instance.is_running()
return False
def broadcast_event(event_type, data):
"""Broadcast event to all connected clients"""
try:
for handler, client_id in _clients:
try:
handler._send_sse_event(event_type, data)
except Exception as e:
logger.error(f"Error broadcasting event to client {client_id}: {e}")
except Exception as e:
logger.error(f"Error broadcasting event: {e}")