Compare commits

...

10 Commits

Author SHA1 Message Date
0004ec6014 Update 2025-04-18 01:29:18 +08:00
3da72c2629 更新 install.mel 2025-04-17 00:45:06 +08:00
12f2c38db7 添加 README.md 2025-04-17 00:44:09 +08:00
bab06c4c2a 添加 restart_mcp.mel 2025-04-17 00:38:55 +08:00
5bcfb6ede3 更新 server.py 2025-04-17 00:37:48 +08:00
9617a78736 添加 fastapi_server.py 2025-04-17 00:37:26 +08:00
a0c6503644 更新 server.py 2025-04-17 00:22:07 +08:00
1f23898258 更新 http_handler.py 2025-04-17 00:20:15 +08:00
5788640962 更新 __init__.py 2025-04-17 00:18:15 +08:00
a6aa8ac8ce 更新 Debug/test_sse_connection.py 2025-04-17 00:17:27 +08:00
11 changed files with 855 additions and 101 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/packages
/__pycache__

View File

@@ -12,6 +12,6 @@ pause -sec 1;
// Load the plugin
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");

View File

@@ -14,6 +14,7 @@ import http.client
import time
import sys
import socket
import json
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()
initial_data = ""
received_complete_data = False
event_count = 0
max_events = 5 # Read 5 events
# 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:
# Read data in smaller chunks to avoid truncation
chunk = response.read(64).decode('utf-8')
chunk = response.read(128).decode('utf-8')
if chunk:
initial_data += chunk
print(f"Received chunk: {chunk}")
# Count events
if "event:" in chunk:
event_count += 1
# Check if we've received complete JSON data
if '}' in chunk and 'data:' in initial_data:
# Continue reading to ensure we get complete events
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)
if '}' in chunk and 'data:' in initial_data and event_count >= max_events:
received_complete_data = True
break
@@ -97,24 +91,49 @@ def test_sse_connection(host="127.0.0.1", port=4550, path="/", timeout=30):
print("-" * 50)
print(initial_data)
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:
print("Warning: No initial data received after 10 seconds")
print("This doesn't necessarily mean the connection failed.")
print("The server might be configured to not send initial data.")
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
print("\nClosing connection...")
conn.close()

138
README.md Normal file
View 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

View File

@@ -19,4 +19,4 @@ __all__ = [
'start_server',
'stop_server',
'is_server_running'
]
]

414
fastapi_server.py Normal file
View 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)

View File

@@ -461,8 +461,9 @@ class MCPHTTPServer:
"""Start server"""
try:
# Create server - use ThreadingHTTPServer instead of HTTPServer
print(f"Attempting to start HTTP server on {SERVER_HOST}:{self.port}")
self.server = ThreadingHTTPServer((SERVER_HOST, self.port), MCPHTTPHandler)
host = "127.0.0.1" # 强制使用 127.0.0.1 作为绑定地址
print(f"Attempting to start HTTP server on {host}:{self.port}")
self.server = ThreadingHTTPServer((host, self.port), MCPHTTPHandler)
# Get actual port used
_, self.port = self.server.server_address
@@ -474,8 +475,8 @@ class MCPHTTPServer:
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}")
print(f"HTTP server started successfully on {host}:{self.port}")
logger.info(f"HTTP server started on {host}:{self.port}")
return True
except Exception as e:
print(f"Error starting HTTP server: {e}")

View File

@@ -24,6 +24,31 @@ global proc string[] getMCPPaths() {
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
global proc installMCPPlugin() {
string $paths[] = `getMCPPaths`;
@@ -98,6 +123,10 @@ global proc installMCPPlugin() {
evalEcho("loadPlugin \"" + $pluginPath + "\"");
print("Plugin loaded successfully\n");
// Force create MCP menu
print("Creating MCP menu...\n");
evalDeferred("forceMCPMenu()");
// Set plugin to auto load
print("Setting plugin to auto load...\n");
evalEcho("pluginInfo -edit -autoload true \"" + $pluginPath + "\"");
@@ -204,7 +233,7 @@ global proc uninstallMCPPlugin() {
// Show success dialog
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"
-defaultButton "OK";
}

View File

@@ -221,19 +221,41 @@ def configure_port_cmd(*args):
)
return
# Get the plugin path using a more reliable method
# Get plugin path using different methods
import sys
import inspect
# Method 1: Find from sys.path
plugin_path = None
for path in sys.path:
if path.endswith('MayaMCP') and os.path.exists(path):
plugin_path = path
break
# Method 2: Get current script directory
if not plugin_path:
# Fallback to a hardcoded path if needed
plugin_path = "D:/Dev/Tools/MayaMCP"
if not os.path.exists(plugin_path):
raise Exception(f"Could not find plugin path: {plugin_path}")
try:
# Get current module file path
current_file = inspect.getfile(inspect.currentframe())
# 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}")
@@ -361,19 +383,39 @@ def initializePlugin(plugin):
plugin_path = os.path.dirname(plugin_fn.loadPath())
om.MGlobal.displayInfo(f"Plugin path: {plugin_path}")
# Get MayaMCP directory path - fix the path issue
mcp_dir = os.path.join(plugin_path, "MayaMCP")
mcp_dir = mcp_dir.replace('\\', '/') # Ensure forward slashes
om.MGlobal.displayInfo(f"MayaMCP directory: {mcp_dir}")
# Get plugin directory path - support multiple directory names
# Try multiple possible directory names
possible_dirs = ["MayaMCP", "Maya_MCP", "Maya-MCP", "mayamcp"]
mcp_dir = None
if os.path.exists(mcp_dir):
om.MGlobal.displayInfo(f"MayaMCP directory found: {mcp_dir}")
# Add MayaMCP directory to sys.path
# First, check if current directory contains plugin files
if os.path.exists(os.path.join(plugin_path, "server.py")):
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:
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:
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
if plugin_path not in sys.path:
@@ -401,15 +443,31 @@ def initializePlugin(plugin):
om.MGlobal.displayInfo("Attempting to import server module...")
# Add current directory to sys.path
# Cannot use __file__ in Maya plugin, use plugin_path instead
current_dir = plugin_path
if "MayaMCP" in current_dir:
current_dir = current_dir # Already in MayaMCP directory
# Try to use the plugin's own directory
try:
current_dir = os.path.dirname(os.path.abspath(__file__))
except:
current_dir = plugin_path
# 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:
# Try to find MayaMCP directory
mcp_dir = os.path.join(current_dir, "MayaMCP")
if os.path.exists(mcp_dir):
current_dir = mcp_dir
# Try to find the directory containing server.py
possible_dirs = ["MayaMCP", "Maya_MCP", "Maya-MCP", "mayamcp"]
found = False
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
om.MGlobal.displayInfo(f"Using directory: {current_dir}")

18
restart_mcp.mel Normal file
View 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";

163
server.py
View File

@@ -2,8 +2,8 @@
# -*- coding: utf-8 -*-
"""
Maya MCP Server
Core implementation of the Model Context Protocol server for Maya.
Maya MCP Server (FastAPI Implementation)
Core implementation of the Model Context Protocol server for Maya using FastAPI.
Version: 1.0.0
Author: Jeffrey Tsai
@@ -23,13 +23,48 @@ from log_config import get_logger, initialize_logging
initialize_logging()
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:
from http_handler import start_http_server, stop_http_server, is_http_server_running, broadcast_event
logger.info("HTTP handler imported successfully")
except Exception as e:
logger.error(f"Error importing HTTP handler: {e}")
from fastapi_server import start_server as fastapi_start_server
from fastapi_server import stop_server as fastapi_stop_server
from fastapi_server import is_server_running as fastapi_is_server_running
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.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
_server_running = False
@@ -45,51 +80,85 @@ def start_server(port=SERVER_PORT):
Returns:
int: Port number if server started successfully, None otherwise
"""
global SERVER_PORT, _server_running
global _server_running, USE_FASTAPI
try:
# Check if server is already running
if _server_running:
logger.info(f"Server already running on port {SERVER_PORT}")
return SERVER_PORT
if is_server_running():
logger.info(f"Server already running on port {port}")
return port
logger.info(f"Starting MCP server on port {port}...")
# Start HTTP server
http_port = start_http_server(port)
if http_port:
_server_running = True
SERVER_PORT = http_port
logger.info(f"MCP server successfully started on port {SERVER_PORT}")
# Get Maya info
# Start the server based on available implementation
if USE_FASTAPI:
try:
# Try to start FastAPI server
port = fastapi_start_server(port=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}")
try:
# Broadcast Maya info to clients
fastapi_broadcast_event_sync("maya_info", maya_info)
except Exception as e:
logger.error(f"Error broadcasting event: {str(e)}")
# This is not a critical error, so we can continue
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 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(operatingSystem=True),
"product_name": cmds.about(product=True)
'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 event
try:
broadcast_event("maya_info", maya_info)
logger.debug("Maya info event broadcasted")
except Exception as e:
logger.warning(f"Error broadcasting Maya info: {e}")
logger.debug(traceback.format_exc())
except Exception as e:
logger.warning(f"Error getting Maya info: {e}")
logger.debug(traceback.format_exc())
return SERVER_PORT
else:
logger.error(f"Failed to start HTTP server on port {port}")
return None
# 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
except Exception as e:
logger.error(f"Error starting server: {e}")
logger.error(traceback.format_exc())
@@ -208,14 +277,20 @@ def get_scene_info():
scene_info = {
"file": current_file,
"selection": selection,
"objects": objects[:10], # Limit to first 10
"objects": objects,
"cameras": cameras,
"lights": lights
}
logger.debug(f"Scene info retrieved: {len(str(scene_info))} bytes")
logger.debug(f"Scene info: {scene_info}")
return scene_info
except Exception as e:
logger.error(f"Error getting scene info: {e}")
logger.error(traceback.format_exc())
return {"error": str(e)}
return {
"file": "unknown",
"selection": [],
"objects": [],
"cameras": [],
"lights": []
}