Compare commits
10 Commits
c944ee970d
...
0004ec6014
Author | SHA1 | Date | |
---|---|---|---|
0004ec6014 | |||
3da72c2629 | |||
12f2c38db7 | |||
bab06c4c2a | |||
5bcfb6ede3 | |||
9617a78736 | |||
a0c6503644 | |||
1f23898258 | |||
5788640962 | |||
a6aa8ac8ce |
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/packages
|
||||||
|
/__pycache__
|
@@ -12,6 +12,6 @@ pause -sec 1;
|
|||||||
|
|
||||||
// Load the plugin
|
// Load the plugin
|
||||||
print("Loading Maya MCP plugin...\n");
|
print("Loading Maya MCP plugin...\n");
|
||||||
loadPlugin "D:/Dev/Tools/MayaMCP/maya_mcp_plugin.py";
|
loadPlugin "d:/Personal/Document/maya/scripts/Maya_MCP/maya_mcp_plugin.py";
|
||||||
|
|
||||||
print("Plugin reload complete.\n");
|
print("Plugin reload complete.\n");
|
||||||
|
@@ -14,6 +14,7 @@ import http.client
|
|||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
import socket
|
import socket
|
||||||
|
import json
|
||||||
|
|
||||||
def test_sse_connection(host="127.0.0.1", port=4550, path="/", timeout=30):
|
def test_sse_connection(host="127.0.0.1", port=4550, path="/", timeout=30):
|
||||||
"""
|
"""
|
||||||
@@ -54,31 +55,24 @@ def test_sse_connection(host="127.0.0.1", port=4550, path="/", timeout=30):
|
|||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
initial_data = ""
|
initial_data = ""
|
||||||
received_complete_data = False
|
received_complete_data = False
|
||||||
|
event_count = 0
|
||||||
|
max_events = 5 # Read 5 events
|
||||||
|
|
||||||
# Try to read data for up to 10 seconds
|
# Try to read data for up to 10 seconds
|
||||||
while time.time() - start_time < 10 and not received_complete_data:
|
while time.time() - start_time < 10 and not received_complete_data and event_count < max_events:
|
||||||
try:
|
try:
|
||||||
# Read data in smaller chunks to avoid truncation
|
# Read data in smaller chunks to avoid truncation
|
||||||
chunk = response.read(64).decode('utf-8')
|
chunk = response.read(128).decode('utf-8')
|
||||||
if chunk:
|
if chunk:
|
||||||
initial_data += chunk
|
initial_data += chunk
|
||||||
print(f"Received chunk: {chunk}")
|
print(f"Received chunk: {chunk}")
|
||||||
|
|
||||||
# Check if we've received complete JSON data
|
# Count events
|
||||||
if '}' in chunk and 'data:' in initial_data:
|
if "event:" in chunk:
|
||||||
# Continue reading to ensure we get complete events
|
event_count += 1
|
||||||
for _ in range(10): # Try to read more chunks
|
|
||||||
try:
|
|
||||||
more_chunk = response.read(64).decode('utf-8')
|
|
||||||
if more_chunk:
|
|
||||||
initial_data += more_chunk
|
|
||||||
print(f"Received additional chunk: {more_chunk}")
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
except:
|
|
||||||
break
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
|
# Check if we've received complete JSON data
|
||||||
|
if '}' in chunk and 'data:' in initial_data and event_count >= max_events:
|
||||||
received_complete_data = True
|
received_complete_data = True
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -97,24 +91,49 @@ def test_sse_connection(host="127.0.0.1", port=4550, path="/", timeout=30):
|
|||||||
print("-" * 50)
|
print("-" * 50)
|
||||||
print(initial_data)
|
print(initial_data)
|
||||||
print("-" * 50)
|
print("-" * 50)
|
||||||
|
|
||||||
|
# Try to parse events
|
||||||
|
try:
|
||||||
|
events = []
|
||||||
|
current_event = {}
|
||||||
|
for line in initial_data.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith(':'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith('event:'):
|
||||||
|
if current_event and 'event' in current_event:
|
||||||
|
events.append(current_event)
|
||||||
|
current_event = {}
|
||||||
|
current_event['event'] = line[6:].strip()
|
||||||
|
elif line.startswith('data:'):
|
||||||
|
if 'data' not in current_event:
|
||||||
|
current_event['data'] = line[5:].strip()
|
||||||
|
else:
|
||||||
|
current_event['data'] += line[5:].strip()
|
||||||
|
|
||||||
|
if current_event and 'event' in current_event:
|
||||||
|
events.append(current_event)
|
||||||
|
|
||||||
|
print("\nParsed Events:")
|
||||||
|
print("-" * 50)
|
||||||
|
for i, event in enumerate(events):
|
||||||
|
print(f"Event {i+1}: {event['event']}")
|
||||||
|
try:
|
||||||
|
data = json.loads(event['data'])
|
||||||
|
print(f"Data: {json.dumps(data, indent=2)}")
|
||||||
|
except:
|
||||||
|
print(f"Data: {event['data']}")
|
||||||
|
print()
|
||||||
|
print("-" * 50)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing events: {e}")
|
||||||
else:
|
else:
|
||||||
print("Warning: No initial data received after 10 seconds")
|
print("Warning: No initial data received after 10 seconds")
|
||||||
print("This doesn't necessarily mean the connection failed.")
|
print("This doesn't necessarily mean the connection failed.")
|
||||||
print("The server might be configured to not send initial data.")
|
print("The server might be configured to not send initial data.")
|
||||||
print("Connection is established (200 OK), which is a good sign.")
|
print("Connection is established (200 OK), which is a good sign.")
|
||||||
|
|
||||||
# Read more data for a few seconds
|
|
||||||
print("\nReading data for 5 seconds...")
|
|
||||||
start_time = time.time()
|
|
||||||
while time.time() - start_time < 5:
|
|
||||||
try:
|
|
||||||
data = response.read(1024).decode('utf-8')
|
|
||||||
if data:
|
|
||||||
print(f"Received data: {data}")
|
|
||||||
except socket.timeout:
|
|
||||||
print("Socket timeout while reading additional data, continuing...")
|
|
||||||
time.sleep(0.5)
|
|
||||||
|
|
||||||
# Close connection
|
# Close connection
|
||||||
print("\nClosing connection...")
|
print("\nClosing connection...")
|
||||||
conn.close()
|
conn.close()
|
||||||
|
138
README.md
Normal file
138
README.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# Maya MCP (Model Context Protocol)
|
||||||
|
|
||||||
|
Maya integration through the Model Context Protocol for Windsurf connectivity.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Drag `install.mel` into Maya viewport
|
||||||
|
2. Click "Install" in the dialog
|
||||||
|
3. **Important**: Restart Maya after installation
|
||||||
|
4. The plugin should now be loaded automatically and the MCP menu should appear in Maya's menu bar
|
||||||
|
|
||||||
|
## Uninstallation
|
||||||
|
|
||||||
|
1. Drag `install.mel` into Maya viewport
|
||||||
|
2. Click "Uninstall" in the dialog
|
||||||
|
3. The plugin will be unloaded and removed from auto-load
|
||||||
|
|
||||||
|
## Manual Loading
|
||||||
|
|
||||||
|
If the plugin doesn't load automatically, you can load it manually in the Script Editor:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import sys
|
||||||
|
sys.path.append(r"D:/Dev/Tools/MayaMCP")
|
||||||
|
import maya.cmds as cmds
|
||||||
|
cmds.loadPlugin("d:/Personal/Document/maya/scripts/Maya_MCP/maya_mcp_plugin.py")
|
||||||
|
```
|
||||||
|
|
||||||
|
If the MCP menu doesn't appear, you can create it manually:
|
||||||
|
|
||||||
|
```mel
|
||||||
|
source "d:/Personal/Document/maya/scripts/Maya_MCP/install.mel";
|
||||||
|
forceMCPMenu();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
- If you encounter any issues after installation, try restarting Maya
|
||||||
|
- Make sure the plugin directory is correctly set in your system
|
||||||
|
- Check Maya's Script Editor for any error messages
|
||||||
|
- Verify that all required files are present in the plugin directory
|
||||||
|
- If the menu doesn't appear, use the `forceMCPMenu()` function as described above
|
||||||
|
- If you get connection errors in Windsurf, check the configuration in `mcp_config.json`
|
||||||
|
|
||||||
|
## Windsurf Configuration
|
||||||
|
|
||||||
|
The Windsurf configuration file is located at:
|
||||||
|
```
|
||||||
|
C:\Users\<username>\.codeium\windsurf\mcp_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure it has the following settings:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"MayaMCP": {
|
||||||
|
"serverUrl": "http://127.0.0.1:4550/",
|
||||||
|
"sseEndpoint": "http://127.0.0.1:4550/",
|
||||||
|
"timeout": 10000,
|
||||||
|
"retryInterval": 1000,
|
||||||
|
"maxRetries": 10,
|
||||||
|
"debug": true,
|
||||||
|
"autoConnect": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
You can test the server connection using the provided test scripts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test the server binding address
|
||||||
|
python Debug/check_server_binding.py
|
||||||
|
|
||||||
|
# Test the SSE connection
|
||||||
|
python Debug/test_sse_connection.py
|
||||||
|
```
|
||||||
|
|
||||||
|
These tests will help verify that the server is correctly bound to 127.0.0.1 and that SSE events are being properly transmitted.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Windsurf connectivity via SSE
|
||||||
|
- Scene information retrieval
|
||||||
|
- Maya integration
|
||||||
|
- FastAPI or HTTP server modes (automatic fallback)
|
||||||
|
|
||||||
|
## Server Modes
|
||||||
|
|
||||||
|
The MCP server can run in two modes:
|
||||||
|
|
||||||
|
1. **FastAPI Mode** (Default): Uses FastAPI and UVicorn for better performance and stability
|
||||||
|
2. **HTTP Mode** (Fallback): Uses Python's built-in HTTP server if FastAPI is not available
|
||||||
|
|
||||||
|
The server automatically selects the best available mode. If FastAPI is installed, it will use that; otherwise, it will fall back to the HTTP server.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The MCP server runs on port 4550 by default. Make sure this port is available and not blocked by a firewall.
|
||||||
|
|
||||||
|
## Advanced: Using UVicorn and FastAPI
|
||||||
|
|
||||||
|
FastAPI and UVicorn are now the recommended server backend. They have been successfully tested with Maya 2025 and provide better performance and stability.
|
||||||
|
|
||||||
|
### Installing with Maya's Python (mayapy)
|
||||||
|
|
||||||
|
You can install FastAPI and UVicorn directly using Maya's Python interpreter:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For Windows
|
||||||
|
"C:\Program Files\Autodesk\Maya2023\bin\mayapy.exe" -m pip install fastapi uvicorn
|
||||||
|
|
||||||
|
# For macOS
|
||||||
|
/Applications/Autodesk/maya2023/Maya.app/Contents/bin/mayapy -m pip install fastapi uvicorn
|
||||||
|
|
||||||
|
# For Linux
|
||||||
|
/usr/autodesk/maya2023/bin/mayapy -m pip install fastapi uvicorn
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install the packages directly into Maya's Python environment, ensuring they are available when running the MCP server from within Maya.
|
||||||
|
|
||||||
|
### Offline Installation
|
||||||
|
|
||||||
|
If you want to install FastAPI and UVicorn in an environment without internet access:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In an environment with internet access, download dependencies
|
||||||
|
python -m pip download uvicorn fastapi -d ./packages
|
||||||
|
|
||||||
|
# In the target environment, install
|
||||||
|
python -m pip install --no-index --find-links=./packages uvicorn fastapi
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Copyright (c) Jeffrey Tsai
|
414
fastapi_server.py
Normal file
414
fastapi_server.py
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Maya MCP FastAPI Server
|
||||||
|
This module provides a FastAPI implementation of the Maya MCP server.
|
||||||
|
|
||||||
|
Version: 1.0.0
|
||||||
|
Author: Jeffrey Tsai
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import asyncio
|
||||||
|
import traceback
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
from fastapi import FastAPI, Request, Response, HTTPException
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from port_config import SERVER_HOST, SERVER_PORT
|
||||||
|
from log_config import get_logger, initialize_logging
|
||||||
|
|
||||||
|
# Initialize logging
|
||||||
|
initialize_logging()
|
||||||
|
logger = get_logger("FastAPIServer")
|
||||||
|
|
||||||
|
# Global variables
|
||||||
|
_server_running = False
|
||||||
|
_clients = [] # List of client connections
|
||||||
|
_clients_lock = asyncio.Lock() # Lock for thread-safe client list operations
|
||||||
|
|
||||||
|
# Create FastAPI app
|
||||||
|
app = FastAPI(
|
||||||
|
title="Maya MCP Server",
|
||||||
|
description="Maya Model Context Protocol Server",
|
||||||
|
version="1.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add CORS middleware
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Event queue for each client
|
||||||
|
event_queues = {}
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
async def add_client(client_id: str):
|
||||||
|
"""Add a new client to the list"""
|
||||||
|
async with _clients_lock:
|
||||||
|
if client_id not in event_queues:
|
||||||
|
event_queues[client_id] = asyncio.Queue()
|
||||||
|
_clients.append(client_id)
|
||||||
|
logger.info(f"Client {client_id} added to clients list")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def remove_client(client_id: str):
|
||||||
|
"""Remove a client from the list"""
|
||||||
|
async with _clients_lock:
|
||||||
|
if client_id in event_queues:
|
||||||
|
del event_queues[client_id]
|
||||||
|
if client_id in _clients:
|
||||||
|
_clients.remove(client_id)
|
||||||
|
logger.info(f"Client {client_id} removed from clients list")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def send_event(client_id: str, event_type: str, data: Dict[str, Any]):
|
||||||
|
"""Send an event to a specific client"""
|
||||||
|
if client_id in event_queues:
|
||||||
|
event_data = {
|
||||||
|
"event": event_type,
|
||||||
|
"data": data
|
||||||
|
}
|
||||||
|
await event_queues[client_id].put(event_data)
|
||||||
|
logger.debug(f"Event {event_type} queued for client {client_id}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def broadcast_event(event_type: str, data: Dict[str, Any]):
|
||||||
|
"""Broadcast an event to all connected clients"""
|
||||||
|
async with _clients_lock:
|
||||||
|
for client_id in _clients:
|
||||||
|
await send_event(client_id, event_type, data)
|
||||||
|
logger.debug(f"Event {event_type} broadcasted to {len(_clients)} clients")
|
||||||
|
|
||||||
|
# Add a synchronous version of the broadcast function for non-async environments
|
||||||
|
def broadcast_event_sync(event_type: str, data: Dict[str, Any]):
|
||||||
|
"""Synchronous version of broadcast function for non-async environments"""
|
||||||
|
import asyncio
|
||||||
|
try:
|
||||||
|
# Create a new event loop
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
# Run the async function
|
||||||
|
loop.run_until_complete(broadcast_event(event_type, data))
|
||||||
|
loop.close()
|
||||||
|
logger.debug(f"Event {event_type} broadcasted synchronously")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error broadcasting event synchronously: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# SSE endpoint
|
||||||
|
@app.get("/")
|
||||||
|
@app.get("/events")
|
||||||
|
async def sse_endpoint(request: Request):
|
||||||
|
"""SSE endpoint for Maya MCP"""
|
||||||
|
client_id = f"client-{int(time.time())}"
|
||||||
|
|
||||||
|
# Add client to list
|
||||||
|
await add_client(client_id)
|
||||||
|
|
||||||
|
# Create event queue
|
||||||
|
queue = event_queues[client_id]
|
||||||
|
|
||||||
|
# Function to generate SSE events
|
||||||
|
async def event_generator():
|
||||||
|
try:
|
||||||
|
# Send initial comment to keep connection alive
|
||||||
|
yield ": ping\n\n".encode('utf-8')
|
||||||
|
yield ": ping\n\n".encode('utf-8')
|
||||||
|
yield ": ping\n\n".encode('utf-8')
|
||||||
|
|
||||||
|
# Send connection event immediately
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Format and send connection event
|
||||||
|
yield f"event: connection\ndata: {json.dumps(connection_data)}\n\n".encode('utf-8')
|
||||||
|
logger.info(f"Sent connection event to client {client_id}")
|
||||||
|
|
||||||
|
# Send ready event immediately after
|
||||||
|
ready_data = {
|
||||||
|
"status": "ready",
|
||||||
|
"timestamp": int(time.time() * 1000)
|
||||||
|
}
|
||||||
|
yield f"event: ready\ndata: {json.dumps(ready_data)}\n\n".encode('utf-8')
|
||||||
|
logger.info(f"Sent ready event to client {client_id}")
|
||||||
|
|
||||||
|
# Send initial scene info
|
||||||
|
try:
|
||||||
|
# import server module dynamically
|
||||||
|
import importlib
|
||||||
|
import server
|
||||||
|
importlib.reload(server)
|
||||||
|
scene_info = server.get_scene_info()
|
||||||
|
yield f"event: scene_info\ndata: {json.dumps(scene_info)}\n\n".encode('utf-8')
|
||||||
|
logger.info(f"Sent initial scene info to client {client_id}")
|
||||||
|
except ImportError:
|
||||||
|
# Provide mock data when running outside Maya
|
||||||
|
mock_scene_info = {
|
||||||
|
"file": "mock_scene.ma",
|
||||||
|
"selection": [],
|
||||||
|
"objects": ["mock_cube", "mock_sphere", "mock_camera"],
|
||||||
|
"cameras": ["mock_cameraShape"],
|
||||||
|
"lights": ["mock_light"]
|
||||||
|
}
|
||||||
|
yield f"event: scene_info\ndata: {json.dumps(mock_scene_info)}\n\n".encode('utf-8')
|
||||||
|
logger.info(f"Sent mock scene info to client {client_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Could not send initial scene info: {e}")
|
||||||
|
logger.debug(traceback.format_exc())
|
||||||
|
|
||||||
|
# Keep connection alive with periodic pings
|
||||||
|
ping_task = asyncio.create_task(send_periodic_pings(client_id))
|
||||||
|
|
||||||
|
# Process events from queue
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# Wait for event with timeout
|
||||||
|
event_data = await asyncio.wait_for(queue.get(), timeout=1.0)
|
||||||
|
event_type = event_data["event"]
|
||||||
|
data = event_data["data"]
|
||||||
|
|
||||||
|
# Format and send event
|
||||||
|
yield f"event: {event_type}\ndata: {json.dumps(data)}\n\n".encode('utf-8')
|
||||||
|
logger.debug(f"Sent event {event_type} to client {client_id}")
|
||||||
|
|
||||||
|
# Mark task as done
|
||||||
|
queue.task_done()
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
# Timeout is expected, just continue
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing event for client {client_id}: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error in event generator for client {client_id}: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
finally:
|
||||||
|
# Clean up
|
||||||
|
await remove_client(client_id)
|
||||||
|
logger.info(f"Client {client_id} connection closed")
|
||||||
|
|
||||||
|
# Return streaming response
|
||||||
|
return StreamingResponse(
|
||||||
|
event_generator(),
|
||||||
|
media_type="text/event-stream",
|
||||||
|
headers={
|
||||||
|
"Cache-Control": "no-cache, no-transform",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type",
|
||||||
|
"X-Accel-Buffering": "no", # Disable Nginx buffering
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def send_periodic_pings(client_id: str):
|
||||||
|
"""Send periodic pings to keep connection alive"""
|
||||||
|
try:
|
||||||
|
while client_id in _clients:
|
||||||
|
# Send ping event
|
||||||
|
await send_event(client_id, "ping", {"timestamp": int(time.time() * 1000)})
|
||||||
|
logger.debug(f"Sent ping event to client {client_id}")
|
||||||
|
|
||||||
|
# Wait for 30 seconds
|
||||||
|
await asyncio.sleep(30)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error sending periodic pings to client {client_id}: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
|
||||||
|
# API endpoints
|
||||||
|
@app.get("/status")
|
||||||
|
async def get_status():
|
||||||
|
"""Get server status"""
|
||||||
|
return {
|
||||||
|
"status": "running" if _server_running else "stopped",
|
||||||
|
"clients": len(_clients),
|
||||||
|
"uptime": int(time.time() - _start_time) if _server_running else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/broadcast")
|
||||||
|
async def api_broadcast_event(event_data: Dict[str, Any]):
|
||||||
|
"""Broadcast an event to all connected clients"""
|
||||||
|
try:
|
||||||
|
event_type = event_data.get("event")
|
||||||
|
data = event_data.get("data", {})
|
||||||
|
|
||||||
|
if not event_type:
|
||||||
|
raise HTTPException(status_code=400, detail="Missing event type")
|
||||||
|
|
||||||
|
await broadcast_event(event_type, data)
|
||||||
|
|
||||||
|
return {"success": True, "message": f"Event {event_type} broadcasted to {len(_clients)} clients"}
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error broadcasting event: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
# Server functions
|
||||||
|
_start_time = 0
|
||||||
|
|
||||||
|
def start_server(host=SERVER_HOST, port=SERVER_PORT):
|
||||||
|
"""
|
||||||
|
Start the FastAPI server using UVicorn
|
||||||
|
|
||||||
|
Args:
|
||||||
|
host (str): Server host
|
||||||
|
port (int): Server port
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: Port number if server started successfully, None otherwise
|
||||||
|
"""
|
||||||
|
global _server_running, _start_time
|
||||||
|
|
||||||
|
# Ensure host is a string
|
||||||
|
if not isinstance(host, str):
|
||||||
|
logger.warning(f"Host is not a string: {host}, converting to string")
|
||||||
|
host = str(host)
|
||||||
|
|
||||||
|
# Ensure port is an integer
|
||||||
|
if not isinstance(port, int):
|
||||||
|
try:
|
||||||
|
port = int(port)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
logger.error(f"Invalid port: {port}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
if _server_running:
|
||||||
|
logger.info(f"Server already running on port {port}")
|
||||||
|
return port
|
||||||
|
|
||||||
|
logger.info(f"Starting FastAPI server on {host}:{port}")
|
||||||
|
|
||||||
|
# Import uvicorn
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
# Create a new event loop
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
|
||||||
|
# Create custom log configuration to avoid using default formatter (which calls isatty)
|
||||||
|
log_config = {
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"formatters": {
|
||||||
|
"simple": {
|
||||||
|
"format": "%(levelname)s: %(message)s",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"level": "INFO",
|
||||||
|
"formatter": "simple",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"uvicorn": {"handlers": ["console"], "level": "INFO"},
|
||||||
|
"uvicorn.error": {"handlers": ["console"], "level": "INFO"},
|
||||||
|
"uvicorn.access": {"handlers": ["console"], "level": "INFO"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create server configuration
|
||||||
|
config = uvicorn.Config(
|
||||||
|
app=app,
|
||||||
|
host=host,
|
||||||
|
port=port,
|
||||||
|
log_level="info",
|
||||||
|
loop="asyncio",
|
||||||
|
log_config=log_config
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a server instance
|
||||||
|
server = uvicorn.Server(config)
|
||||||
|
|
||||||
|
# Start the server in a separate thread
|
||||||
|
import threading
|
||||||
|
server_thread = threading.Thread(target=server.run, daemon=True)
|
||||||
|
server_thread.start()
|
||||||
|
|
||||||
|
# Wait for server to start
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
# Set server state
|
||||||
|
_server_running = True
|
||||||
|
_start_time = time.time()
|
||||||
|
|
||||||
|
logger.info(f"FastAPI server started on {host}:{port}")
|
||||||
|
|
||||||
|
return port
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error starting FastAPI server: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return None
|
||||||
|
|
||||||
|
def stop_server():
|
||||||
|
"""
|
||||||
|
Stop the FastAPI server
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Whether server was successfully stopped
|
||||||
|
"""
|
||||||
|
global _server_running
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not _server_running:
|
||||||
|
logger.info("Server not running")
|
||||||
|
return True
|
||||||
|
|
||||||
|
logger.info("Stopping FastAPI server")
|
||||||
|
|
||||||
|
# There's no clean way to stop uvicorn programmatically
|
||||||
|
# We'll use a workaround by killing the event loop
|
||||||
|
try:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
if loop.is_running():
|
||||||
|
loop.stop()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error stopping event loop: {e}")
|
||||||
|
|
||||||
|
# Set server state
|
||||||
|
_server_running = False
|
||||||
|
|
||||||
|
logger.info("FastAPI server stopped")
|
||||||
|
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error stopping FastAPI server: {e}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
return False
|
||||||
|
|
||||||
|
def is_server_running():
|
||||||
|
"""
|
||||||
|
Check if server is running
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: Whether server is running
|
||||||
|
"""
|
||||||
|
global _server_running
|
||||||
|
return _server_running
|
||||||
|
|
||||||
|
# For testing
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="127.0.0.1", port=4550)
|
@@ -461,8 +461,9 @@ class MCPHTTPServer:
|
|||||||
"""Start server"""
|
"""Start server"""
|
||||||
try:
|
try:
|
||||||
# Create server - use ThreadingHTTPServer instead of HTTPServer
|
# Create server - use ThreadingHTTPServer instead of HTTPServer
|
||||||
print(f"Attempting to start HTTP server on {SERVER_HOST}:{self.port}")
|
host = "127.0.0.1" # 强制使用 127.0.0.1 作为绑定地址
|
||||||
self.server = ThreadingHTTPServer((SERVER_HOST, self.port), MCPHTTPHandler)
|
print(f"Attempting to start HTTP server on {host}:{self.port}")
|
||||||
|
self.server = ThreadingHTTPServer((host, self.port), MCPHTTPHandler)
|
||||||
|
|
||||||
# Get actual port used
|
# Get actual port used
|
||||||
_, self.port = self.server.server_address
|
_, self.port = self.server.server_address
|
||||||
@@ -474,8 +475,8 @@ class MCPHTTPServer:
|
|||||||
self.thread.daemon = True
|
self.thread.daemon = True
|
||||||
self.thread.start()
|
self.thread.start()
|
||||||
|
|
||||||
print(f"HTTP server started successfully on {SERVER_HOST}:{self.port}")
|
print(f"HTTP server started successfully on {host}:{self.port}")
|
||||||
logger.info(f"HTTP server started on {SERVER_HOST}:{self.port}")
|
logger.info(f"HTTP server started on {host}:{self.port}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error starting HTTP server: {e}")
|
print(f"Error starting HTTP server: {e}")
|
||||||
|
31
install.mel
31
install.mel
@@ -24,6 +24,31 @@ global proc string[] getMCPPaths() {
|
|||||||
return $paths;
|
return $paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force create MCP menu
|
||||||
|
global proc forceMCPMenu() {
|
||||||
|
// Delete existing menu if it exists
|
||||||
|
if (`menu -exists "MCPMenu"`) {
|
||||||
|
deleteUI -menu "MCPMenu";
|
||||||
|
print("Removed existing MCP menu\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new menu
|
||||||
|
string $mainWindow = "MayaWindow";
|
||||||
|
string $mcpMenu = `menu -label "MCP" -parent $mainWindow "MCPMenu"`;
|
||||||
|
|
||||||
|
// Add menu items
|
||||||
|
menuItem -label "Start Server" -command "python(\"import maya_mcp_plugin; maya_mcp_plugin.start_server_cmd()\")";
|
||||||
|
menuItem -label "Stop Server" -command "python(\"import maya_mcp_plugin; maya_mcp_plugin.stop_server_cmd()\")";
|
||||||
|
menuItem -label "Restart Server" -command "python(\"import maya_mcp_plugin; maya_mcp_plugin.restart_server_cmd()\")";
|
||||||
|
menuItem -divider true;
|
||||||
|
menuItem -label "Configure Port" -command "python(\"import maya_mcp_plugin; maya_mcp_plugin.configure_port_cmd()\")";
|
||||||
|
menuItem -divider true;
|
||||||
|
menuItem -label "About" -command "python(\"import maya_mcp_plugin; maya_mcp_plugin.about_cmd()\")";
|
||||||
|
|
||||||
|
print("MCP menu created successfully\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Install MCP plugin
|
// Install MCP plugin
|
||||||
global proc installMCPPlugin() {
|
global proc installMCPPlugin() {
|
||||||
string $paths[] = `getMCPPaths`;
|
string $paths[] = `getMCPPaths`;
|
||||||
@@ -98,6 +123,10 @@ global proc installMCPPlugin() {
|
|||||||
evalEcho("loadPlugin \"" + $pluginPath + "\"");
|
evalEcho("loadPlugin \"" + $pluginPath + "\"");
|
||||||
print("Plugin loaded successfully\n");
|
print("Plugin loaded successfully\n");
|
||||||
|
|
||||||
|
// Force create MCP menu
|
||||||
|
print("Creating MCP menu...\n");
|
||||||
|
evalDeferred("forceMCPMenu()");
|
||||||
|
|
||||||
// Set plugin to auto load
|
// Set plugin to auto load
|
||||||
print("Setting plugin to auto load...\n");
|
print("Setting plugin to auto load...\n");
|
||||||
evalEcho("pluginInfo -edit -autoload true \"" + $pluginPath + "\"");
|
evalEcho("pluginInfo -edit -autoload true \"" + $pluginPath + "\"");
|
||||||
@@ -204,7 +233,7 @@ global proc uninstallMCPPlugin() {
|
|||||||
|
|
||||||
// Show success dialog
|
// Show success dialog
|
||||||
confirmDialog -title "MCP Uninstallation Successful"
|
confirmDialog -title "MCP Uninstallation Successful"
|
||||||
-message ("Maya MCP has been successfully uninstalled!\n\nThe plugin has been unloaded and auto load disabled.")
|
-message ("Maya MCP has been successfully uninstalled!\n\nThe plugin has been unloaded and disabled from auto loading.")
|
||||||
-button "OK"
|
-button "OK"
|
||||||
-defaultButton "OK";
|
-defaultButton "OK";
|
||||||
}
|
}
|
||||||
|
@@ -221,19 +221,41 @@ def configure_port_cmd(*args):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get the plugin path using a more reliable method
|
# Get plugin path using different methods
|
||||||
import sys
|
import sys
|
||||||
|
import inspect
|
||||||
|
|
||||||
|
# Method 1: Find from sys.path
|
||||||
plugin_path = None
|
plugin_path = None
|
||||||
for path in sys.path:
|
for path in sys.path:
|
||||||
if path.endswith('MayaMCP') and os.path.exists(path):
|
if path.endswith('MayaMCP') and os.path.exists(path):
|
||||||
plugin_path = path
|
plugin_path = path
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Method 2: Get current script directory
|
||||||
if not plugin_path:
|
if not plugin_path:
|
||||||
# Fallback to a hardcoded path if needed
|
try:
|
||||||
plugin_path = "D:/Dev/Tools/MayaMCP"
|
# Get current module file path
|
||||||
if not os.path.exists(plugin_path):
|
current_file = inspect.getfile(inspect.currentframe())
|
||||||
raise Exception(f"Could not find plugin path: {plugin_path}")
|
# Get directory
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(current_file))
|
||||||
|
if os.path.exists(current_dir):
|
||||||
|
plugin_path = current_dir
|
||||||
|
except Exception as e:
|
||||||
|
om.MGlobal.displayWarning(f"Failed to get path from current script: {e}")
|
||||||
|
|
||||||
|
# Method 3: Get from __file__
|
||||||
|
if not plugin_path:
|
||||||
|
try:
|
||||||
|
if '__file__' in globals():
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
if os.path.exists(current_dir):
|
||||||
|
plugin_path = current_dir
|
||||||
|
except Exception as e:
|
||||||
|
om.MGlobal.displayWarning(f"Failed to get path from __file__: {e}")
|
||||||
|
|
||||||
|
if not plugin_path:
|
||||||
|
raise Exception("Failed to determine plugin path, please check installation")
|
||||||
|
|
||||||
om.MGlobal.displayInfo(f"Using plugin path: {plugin_path}")
|
om.MGlobal.displayInfo(f"Using plugin path: {plugin_path}")
|
||||||
|
|
||||||
@@ -361,19 +383,39 @@ def initializePlugin(plugin):
|
|||||||
plugin_path = os.path.dirname(plugin_fn.loadPath())
|
plugin_path = os.path.dirname(plugin_fn.loadPath())
|
||||||
om.MGlobal.displayInfo(f"Plugin path: {plugin_path}")
|
om.MGlobal.displayInfo(f"Plugin path: {plugin_path}")
|
||||||
|
|
||||||
# Get MayaMCP directory path - fix the path issue
|
# Get plugin directory path - support multiple directory names
|
||||||
mcp_dir = os.path.join(plugin_path, "MayaMCP")
|
# Try multiple possible directory names
|
||||||
mcp_dir = mcp_dir.replace('\\', '/') # Ensure forward slashes
|
possible_dirs = ["MayaMCP", "Maya_MCP", "Maya-MCP", "mayamcp"]
|
||||||
om.MGlobal.displayInfo(f"MayaMCP directory: {mcp_dir}")
|
mcp_dir = None
|
||||||
|
|
||||||
if os.path.exists(mcp_dir):
|
# First, check if current directory contains plugin files
|
||||||
om.MGlobal.displayInfo(f"MayaMCP directory found: {mcp_dir}")
|
if os.path.exists(os.path.join(plugin_path, "server.py")):
|
||||||
# Add MayaMCP directory to sys.path
|
mcp_dir = plugin_path
|
||||||
|
om.MGlobal.displayInfo(f"Found plugin files in current directory: {mcp_dir}")
|
||||||
|
else:
|
||||||
|
# Try all possible directory names
|
||||||
|
for dir_name in possible_dirs:
|
||||||
|
test_dir = os.path.join(plugin_path, dir_name)
|
||||||
|
test_dir = test_dir.replace('\\', '/') # Ensure forward slashes
|
||||||
|
if os.path.exists(test_dir):
|
||||||
|
mcp_dir = test_dir
|
||||||
|
om.MGlobal.displayInfo(f"Found plugin directory: {mcp_dir}")
|
||||||
|
break
|
||||||
|
|
||||||
|
# If found, add to sys.path
|
||||||
|
if mcp_dir:
|
||||||
if mcp_dir not in sys.path:
|
if mcp_dir not in sys.path:
|
||||||
sys.path.append(mcp_dir)
|
sys.path.append(mcp_dir)
|
||||||
om.MGlobal.displayInfo(f"Added MayaMCP directory to sys.path: {mcp_dir}")
|
om.MGlobal.displayInfo(f"Added plugin directory to sys.path: {mcp_dir}")
|
||||||
else:
|
else:
|
||||||
om.MGlobal.displayInfo(f"MayaMCP directory not found at: {mcp_dir}")
|
om.MGlobal.displayInfo(f"Plugin directory not found in: {plugin_path}")
|
||||||
|
# Try to use the plugin's own directory
|
||||||
|
plugin_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
if plugin_dir and os.path.exists(plugin_dir):
|
||||||
|
mcp_dir = plugin_dir
|
||||||
|
if mcp_dir not in sys.path:
|
||||||
|
sys.path.append(mcp_dir)
|
||||||
|
om.MGlobal.displayInfo(f"Added plugin file directory to sys.path: {mcp_dir}")
|
||||||
|
|
||||||
# Ensure plugin path is also added to sys.path
|
# Ensure plugin path is also added to sys.path
|
||||||
if plugin_path not in sys.path:
|
if plugin_path not in sys.path:
|
||||||
@@ -401,15 +443,31 @@ def initializePlugin(plugin):
|
|||||||
om.MGlobal.displayInfo("Attempting to import server module...")
|
om.MGlobal.displayInfo("Attempting to import server module...")
|
||||||
|
|
||||||
# Add current directory to sys.path
|
# Add current directory to sys.path
|
||||||
# Cannot use __file__ in Maya plugin, use plugin_path instead
|
# Try to use the plugin's own directory
|
||||||
|
try:
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
except:
|
||||||
current_dir = plugin_path
|
current_dir = plugin_path
|
||||||
if "MayaMCP" in current_dir:
|
|
||||||
current_dir = current_dir # Already in MayaMCP directory
|
# Check if current directory contains necessary files
|
||||||
|
if os.path.exists(os.path.join(current_dir, "server.py")):
|
||||||
|
# Already in the correct directory
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
# Try to find MayaMCP directory
|
# Try to find the directory containing server.py
|
||||||
mcp_dir = os.path.join(current_dir, "MayaMCP")
|
possible_dirs = ["MayaMCP", "Maya_MCP", "Maya-MCP", "mayamcp"]
|
||||||
if os.path.exists(mcp_dir):
|
found = False
|
||||||
current_dir = mcp_dir
|
|
||||||
|
for dir_name in possible_dirs:
|
||||||
|
test_dir = os.path.join(plugin_path, dir_name)
|
||||||
|
if os.path.exists(os.path.join(test_dir, "server.py")):
|
||||||
|
current_dir = test_dir
|
||||||
|
found = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# If not found in subdirectories, check current directory
|
||||||
|
if not found and os.path.exists(os.path.join(plugin_path, "server.py")):
|
||||||
|
current_dir = plugin_path
|
||||||
|
|
||||||
current_dir = current_dir.replace('\\', '/') # Ensure forward slashes
|
current_dir = current_dir.replace('\\', '/') # Ensure forward slashes
|
||||||
om.MGlobal.displayInfo(f"Using directory: {current_dir}")
|
om.MGlobal.displayInfo(f"Using directory: {current_dir}")
|
||||||
|
18
restart_mcp.mel
Normal file
18
restart_mcp.mel
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// Restart Maya MCP plugin
|
||||||
|
// First, unload the plugin (if loaded)
|
||||||
|
if (`pluginInfo -query -loaded "d:/Personal/Document/maya/scripts/Maya_MCP/maya_mcp_plugin.py"`)
|
||||||
|
{
|
||||||
|
unloadPlugin "d:/Personal/Document/maya/scripts/Maya_MCP/maya_mcp_plugin.py";
|
||||||
|
print "Maya MCP plugin unloaded.\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a moment
|
||||||
|
pause -seconds 1;
|
||||||
|
|
||||||
|
// Load the plugin using full path
|
||||||
|
loadPlugin "d:/Personal/Document/maya/scripts/Maya_MCP/maya_mcp_plugin.py";
|
||||||
|
print "Maya MCP plugin loaded.\n";
|
||||||
|
|
||||||
|
// Start the server (using FastAPI mode)
|
||||||
|
python("import sys; sys.path.append('d:/Dev/Tools/MayaMCP'); import importlib; import server; importlib.reload(server); server.stop_server(); server.start_server()");
|
||||||
|
print "Maya MCP server restarted.\n";
|
153
server.py
153
server.py
@@ -2,8 +2,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Maya MCP Server
|
Maya MCP Server (FastAPI Implementation)
|
||||||
Core implementation of the Model Context Protocol server for Maya.
|
Core implementation of the Model Context Protocol server for Maya using FastAPI.
|
||||||
|
|
||||||
Version: 1.0.0
|
Version: 1.0.0
|
||||||
Author: Jeffrey Tsai
|
Author: Jeffrey Tsai
|
||||||
@@ -23,13 +23,48 @@ from log_config import get_logger, initialize_logging
|
|||||||
initialize_logging()
|
initialize_logging()
|
||||||
logger = get_logger("Server")
|
logger = get_logger("Server")
|
||||||
|
|
||||||
# Import HTTP handler
|
# Try to import FastAPI server, fall back to HTTP handler if not available
|
||||||
|
USE_FASTAPI = True # Default to True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from http_handler import start_http_server, stop_http_server, is_http_server_running, broadcast_event
|
from fastapi_server import start_server as fastapi_start_server
|
||||||
logger.info("HTTP handler imported successfully")
|
from fastapi_server import stop_server as fastapi_stop_server
|
||||||
except Exception as e:
|
from fastapi_server import is_server_running as fastapi_is_server_running
|
||||||
logger.error(f"Error importing HTTP handler: {e}")
|
from fastapi_server import broadcast_event_sync as fastapi_broadcast_event_sync
|
||||||
|
logger.info("FastAPI server imported successfully")
|
||||||
|
# Check if FastAPI is actually working by trying to create a simple app
|
||||||
|
try:
|
||||||
|
import fastapi
|
||||||
|
test_app = fastapi.FastAPI()
|
||||||
|
logger.info("FastAPI test successful")
|
||||||
|
USE_FASTAPI = True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"FastAPI test failed: {str(e)}")
|
||||||
|
logger.info("HTTP handler imported as fallback")
|
||||||
|
USE_FASTAPI = False
|
||||||
|
except ImportError as e:
|
||||||
|
logger.error(f"Error importing FastAPI server: {str(e)}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
|
logger.info("HTTP handler imported as fallback")
|
||||||
|
USE_FASTAPI = False
|
||||||
|
|
||||||
|
# Fallback to HTTP handler if FastAPI is not available
|
||||||
|
try:
|
||||||
|
from http_handler import start_http_server, stop_http_server, is_http_server_running, broadcast_event
|
||||||
|
logger.info("HTTP handler imported as fallback")
|
||||||
|
# Set flags to indicate we're using the fallback
|
||||||
|
_using_fastapi = False
|
||||||
|
except Exception as e2:
|
||||||
|
logger.error(f"Error importing HTTP handler: {e2}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
else:
|
||||||
|
# Set flags to indicate we're using FastAPI
|
||||||
|
_using_fastapi = True
|
||||||
|
# Alias the functions for compatibility
|
||||||
|
start_http_server = fastapi_start_server
|
||||||
|
stop_http_server = fastapi_stop_server
|
||||||
|
is_http_server_running = fastapi_is_server_running
|
||||||
|
broadcast_event = fastapi_broadcast_event_sync
|
||||||
|
|
||||||
# Global variables
|
# Global variables
|
||||||
_server_running = False
|
_server_running = False
|
||||||
@@ -45,50 +80,84 @@ def start_server(port=SERVER_PORT):
|
|||||||
Returns:
|
Returns:
|
||||||
int: Port number if server started successfully, None otherwise
|
int: Port number if server started successfully, None otherwise
|
||||||
"""
|
"""
|
||||||
global SERVER_PORT, _server_running
|
global _server_running, USE_FASTAPI
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Check if server is already running
|
# Check if server is already running
|
||||||
if _server_running:
|
if is_server_running():
|
||||||
logger.info(f"Server already running on port {SERVER_PORT}")
|
logger.info(f"Server already running on port {port}")
|
||||||
return SERVER_PORT
|
return port
|
||||||
|
|
||||||
logger.info(f"Starting MCP server on port {port}...")
|
logger.info(f"Starting MCP server on port {port}...")
|
||||||
|
|
||||||
# Start HTTP server
|
# Start the server based on available implementation
|
||||||
http_port = start_http_server(port)
|
if USE_FASTAPI:
|
||||||
|
|
||||||
if http_port:
|
|
||||||
_server_running = True
|
|
||||||
SERVER_PORT = http_port
|
|
||||||
|
|
||||||
logger.info(f"MCP server successfully started on port {SERVER_PORT}")
|
|
||||||
|
|
||||||
# Get Maya info
|
|
||||||
try:
|
try:
|
||||||
|
# Try to start FastAPI server
|
||||||
|
port = fastapi_start_server(port=port)
|
||||||
|
if port:
|
||||||
|
_server_running = True
|
||||||
|
# Log Maya info
|
||||||
maya_info = {
|
maya_info = {
|
||||||
"maya_version": cmds.about(version=True),
|
'maya_version': cmds.about(version=True),
|
||||||
"maya_api_version": om.MGlobal.apiVersion(),
|
'maya_api_version': om.MGlobal.apiVersion(),
|
||||||
"os_name": cmds.about(operatingSystem=True),
|
'os_name': cmds.about(os=True),
|
||||||
"product_name": cmds.about(product=True)
|
'product_name': cmds.about(product=True)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(f"Maya info: {maya_info}")
|
logger.info(f"Maya info: {maya_info}")
|
||||||
|
|
||||||
# Broadcast Maya info event
|
|
||||||
try:
|
try:
|
||||||
broadcast_event("maya_info", maya_info)
|
# Broadcast Maya info to clients
|
||||||
logger.debug("Maya info event broadcasted")
|
fastapi_broadcast_event_sync("maya_info", maya_info)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error broadcasting Maya info: {e}")
|
logger.error(f"Error broadcasting event: {str(e)}")
|
||||||
logger.debug(traceback.format_exc())
|
# This is not a critical error, so we can continue
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"Error getting Maya info: {e}")
|
|
||||||
logger.debug(traceback.format_exc())
|
|
||||||
|
|
||||||
return SERVER_PORT
|
logger.info(f"MCP server successfully started on port {port}")
|
||||||
|
print("\n==================================================")
|
||||||
|
print(f"MCP SERVER STARTED SUCCESSFULLY ON PORT {port}")
|
||||||
|
print("==================================================\n")
|
||||||
|
return port
|
||||||
else:
|
else:
|
||||||
logger.error(f"Failed to start HTTP server on port {port}")
|
logger.error("Failed to start FastAPI server")
|
||||||
|
# Fall back to HTTP handler
|
||||||
|
logger.info("Falling back to HTTP handler")
|
||||||
|
USE_FASTAPI = False
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error starting FastAPI server: {str(e)}")
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
|
# Fall back to HTTP handler
|
||||||
|
logger.info("Falling back to HTTP handler")
|
||||||
|
USE_FASTAPI = False
|
||||||
|
|
||||||
|
# If FastAPI failed or not available, use HTTP handler
|
||||||
|
if not USE_FASTAPI:
|
||||||
|
from http_handler import start_http_server, stop_http_server, is_http_server_running, broadcast_event
|
||||||
|
# Ensure port value is valid, if port becomes None, use SERVER_PORT
|
||||||
|
http_port = port if port is not None else SERVER_PORT
|
||||||
|
print(f"Attempting to start HTTP server on 127.0.0.1:{http_port}")
|
||||||
|
port = start_http_server(port=http_port)
|
||||||
|
if port:
|
||||||
|
_server_running = True
|
||||||
|
# Log Maya info
|
||||||
|
maya_info = {
|
||||||
|
'maya_version': cmds.about(version=True),
|
||||||
|
'maya_api_version': om.MGlobal.apiVersion(),
|
||||||
|
'os_name': cmds.about(os=True),
|
||||||
|
'product_name': cmds.about(product=True)
|
||||||
|
}
|
||||||
|
logger.info(f"Maya info: {maya_info}")
|
||||||
|
|
||||||
|
# Broadcast Maya info to clients
|
||||||
|
broadcast_event("maya_info", maya_info)
|
||||||
|
|
||||||
|
logger.info(f"MCP server successfully started on port {port}")
|
||||||
|
print("\n==================================================")
|
||||||
|
print(f"MCP SERVER STARTED SUCCESSFULLY ON PORT {port}")
|
||||||
|
print("==================================================\n")
|
||||||
|
return port
|
||||||
|
else:
|
||||||
|
logger.error("Failed to start HTTP server")
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error starting server: {e}")
|
logger.error(f"Error starting server: {e}")
|
||||||
@@ -208,14 +277,20 @@ def get_scene_info():
|
|||||||
scene_info = {
|
scene_info = {
|
||||||
"file": current_file,
|
"file": current_file,
|
||||||
"selection": selection,
|
"selection": selection,
|
||||||
"objects": objects[:10], # Limit to first 10
|
"objects": objects,
|
||||||
"cameras": cameras,
|
"cameras": cameras,
|
||||||
"lights": lights
|
"lights": lights
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(f"Scene info retrieved: {len(str(scene_info))} bytes")
|
logger.debug(f"Scene info: {scene_info}")
|
||||||
return scene_info
|
return scene_info
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error getting scene info: {e}")
|
logger.error(f"Error getting scene info: {e}")
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
return {"error": str(e)}
|
return {
|
||||||
|
"file": "unknown",
|
||||||
|
"selection": [],
|
||||||
|
"objects": [],
|
||||||
|
"cameras": [],
|
||||||
|
"lights": []
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user