Files
Maya_MCP/maya_mcp_plugin.py
2025-04-18 01:29:18 +08:00

555 lines
21 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Maya MCP Plugin
This plugin provides integration with the Model Context Protocol for Maya.
To load this plugin:
1. Open Maya's Plugin Manager (Windows > Settings/Preferences > Plug-in Manager)
2. Click "Browse" and select this file
3. Check "Loaded" and "Auto load"
Version: 1.0.0
Author: Jeffrey Tsai
"""
import sys
import os
import maya.api.OpenMaya as om
import maya.cmds as cmds
import traceback
import time
import inspect
# Plugin information
def maya_useNewAPI():
"""
Tell Maya this plugin uses the Python API 2.0
"""
pass
# Global variables
_mcp_menu = None
def create_menu():
"""
Create the MCP menu in Maya
Returns:
str: Menu name if created successfully, None otherwise
"""
global _mcp_menu
try:
# Delete existing menu if it exists
if _mcp_menu and cmds.menu(_mcp_menu, exists=True):
cmds.deleteUI(_mcp_menu)
# Get main window
main_window = "MayaWindow"
# Create menu
_mcp_menu = cmds.menu("MCPMenu", label="MCP", parent=main_window)
# Add menu items
cmds.menuItem(label="Start Server", command=lambda x: start_server_cmd())
cmds.menuItem(label="Stop Server", command=lambda x: stop_server_cmd())
cmds.menuItem(label="Restart Server", command=lambda x: restart_server_cmd())
cmds.menuItem(divider=True)
cmds.menuItem(label="Configure Port", command=lambda x: configure_port_cmd())
cmds.menuItem(divider=True)
cmds.menuItem(label="About", command=lambda x: about_cmd())
om.MGlobal.displayInfo("MCP menu created successfully")
return _mcp_menu
except Exception as e:
om.MGlobal.displayError(f"Error creating MCP menu: {e}")
return None
def start_server_cmd(*args):
"""Command to start the MCP server"""
try:
import server
if server.is_server_running():
cmds.confirmDialog(
title="MCP Server",
message="MCP server is already running",
button="OK"
)
return
port = server.start_server()
if port:
cmds.inViewMessage(
message=f"MCP server started on port {port}",
position="topCenter",
fade=True,
fadeOutTime=5.0,
backColor=(0.2, 0.6, 0.2),
fontSize=24
)
else:
cmds.confirmDialog(
title="MCP Server Error",
message="Failed to start MCP server",
button="OK"
)
except Exception as e:
error_msg = f"Error starting MCP server: {e}"
om.MGlobal.displayError(error_msg)
cmds.confirmDialog(
title="MCP Server Error",
message=error_msg,
button="OK"
)
def stop_server_cmd(*args):
"""Command to stop the MCP server"""
try:
import server
if not server.is_server_running():
cmds.confirmDialog(
title="MCP Server",
message="MCP server is not running",
button="OK"
)
return
success = server.stop_server()
if success:
cmds.inViewMessage(
message="MCP server stopped",
position="topCenter",
fade=True,
fadeOutTime=5.0,
backColor=(0.6, 0.2, 0.2),
fontSize=24
)
else:
cmds.confirmDialog(
title="MCP Server Error",
message="Failed to stop MCP server",
button="OK"
)
except Exception as e:
error_msg = f"Error stopping MCP server: {e}"
om.MGlobal.displayError(error_msg)
cmds.confirmDialog(
title="MCP Server Error",
message=error_msg,
button="OK"
)
def restart_server_cmd(*args):
"""Command to restart the MCP server"""
try:
import server
# Stop the server
om.MGlobal.displayInfo("Stopping MCP server...")
server.stop_server()
# Wait for a moment to ensure the server is fully stopped
time.sleep(1)
# Start the server
om.MGlobal.displayInfo("Starting MCP server...")
port = server.start_server()
if port:
cmds.inViewMessage(
message=f"MCP server restarted on port {port}",
position="topCenter",
fade=True,
fadeOutTime=5.0,
backColor=(0.2, 0.6, 0.2),
fontSize=24
)
om.MGlobal.displayInfo(f"MCP server successfully restarted on port {port}")
else:
cmds.confirmDialog(
title="MCP Server Error",
message="Failed to restart MCP server",
button="OK"
)
om.MGlobal.displayError("Failed to restart MCP server")
except Exception as e:
error_msg = f"Error restarting MCP server: {e}"
om.MGlobal.displayError(error_msg)
cmds.confirmDialog(
title="MCP Server Error",
message=error_msg,
button="OK"
)
def configure_port_cmd(*args):
"""Command to configure server port"""
try:
# Import port_config to get current port
import port_config
current_port = port_config.SERVER_PORT
# Show dialog to input new port
result = cmds.promptDialog(
title='Configure MCP Port',
message='Enter new port number:',
text=str(current_port),
button=['OK', 'Cancel'],
defaultButton='OK',
cancelButton='Cancel',
dismissString='Cancel'
)
if result == 'OK':
# Get the new port number
new_port_str = cmds.promptDialog(query=True, text=True)
try:
new_port = int(new_port_str)
# Validate port number
if new_port < 1024 or new_port > 65535:
cmds.confirmDialog(
title="Invalid Port",
message="Port number must be between 1024 and 65535",
button="OK"
)
return
# 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:
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}")
# Update port_config.py file
port_config_path = os.path.join(plugin_path, "port_config.py")
om.MGlobal.displayInfo(f"Port config path: {port_config_path}")
# Read the current content
with open(port_config_path, 'r') as f:
content = f.read()
# Replace the port number
import re
new_content = re.sub(
r'SERVER_PORT\s*=\s*\d+',
f'SERVER_PORT = {new_port}',
content
)
# Write the updated content
with open(port_config_path, 'w') as f:
f.write(new_content)
# Reload port_config module to apply changes
import importlib
if 'port_config' in sys.modules:
importlib.reload(sys.modules['port_config'])
om.MGlobal.displayInfo("Reloaded port_config module")
# Update global variable
import port_config
port_config.SERVER_PORT = new_port
om.MGlobal.displayInfo(f"Updated SERVER_PORT to {port_config.SERVER_PORT}")
# Ask if user wants to restart the server
result = cmds.confirmDialog(
title="Port Updated",
message=f"Port has been updated to {new_port}.\n\nDo you want to restart the server now?",
button=["Yes", "No"],
defaultButton="Yes",
cancelButton="No",
dismissString="No"
)
if result == "Yes":
# Stop current server
import server
om.MGlobal.displayInfo("Stopping current server...")
server.stop_server()
# Wait for server to fully stop
time.sleep(2)
# Reload server module
if 'server' in sys.modules:
importlib.reload(sys.modules['server'])
om.MGlobal.displayInfo("Reloaded server module")
# Reload http_handler module
if 'http_handler' in sys.modules:
importlib.reload(sys.modules['http_handler'])
om.MGlobal.displayInfo("Reloaded http_handler module")
# Start server on new port
om.MGlobal.displayInfo(f"Starting server on new port {new_port}...")
port = server.start_server()
if port == new_port:
cmds.inViewMessage(
message=f"MCP server restarted on port {port}",
position="topCenter",
fade=True,
fadeOutTime=5.0,
backColor=(0.2, 0.6, 0.2),
fontSize=24
)
om.MGlobal.displayInfo(f"Server successfully restarted on port {port}")
else:
cmds.confirmDialog(
title="Port Mismatch",
message=f"Server started on port {port}, but requested port was {new_port}",
button="OK"
)
else:
om.MGlobal.displayInfo(f"MCP port updated to {new_port}. Please restart the server manually.")
except ValueError:
cmds.confirmDialog(
title="Invalid Input",
message="Please enter a valid port number",
button="OK"
)
except Exception as e:
error_msg = f"Error configuring port: {e}"
om.MGlobal.displayError(error_msg)
cmds.confirmDialog(
title="Configuration Error",
message=error_msg,
button="OK"
)
def about_cmd(*args):
"""Command to show about dialog"""
cmds.confirmDialog(
title="About MCP",
message="Maya MCP (Model Context Protocol)\nVersion 1.0.0\n\nAuthor: Jeffrey Tsai",
button="OK"
)
# Plugin initialization
def initializePlugin(plugin):
"""
Initialize the plugin
Args:
plugin: MObject used to register commands
"""
vendor = "Jeffrey Tsai"
version = "1.0.0"
plugin_fn = om.MFnPlugin(plugin, vendor, version)
try:
# Get more reliable plugin path
plugin_path = os.path.dirname(plugin_fn.loadPath())
om.MGlobal.displayInfo(f"Plugin path: {plugin_path}")
# Get plugin directory path - support multiple directory names
# Try multiple possible directory names
possible_dirs = ["MayaMCP", "Maya_MCP", "Maya-MCP", "mayamcp"]
mcp_dir = None
# 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 plugin directory to sys.path: {mcp_dir}")
else:
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:
sys.path.append(plugin_path)
om.MGlobal.displayInfo(f"Added to sys.path: {plugin_path}")
# Print current sys.path for debugging
om.MGlobal.displayInfo(f"Current sys.path: {sys.path}")
# Force reload modules if they are already loaded
# This ensures we're using the latest version of the code
for module_name in ["server", "http_handler", "maya_mcp"]:
if module_name in sys.modules:
om.MGlobal.displayInfo(f"Reloading module: {module_name}")
try:
# Python 3.4+
import importlib
importlib.reload(sys.modules[module_name])
except Exception as e:
om.MGlobal.displayWarning(f"Failed to reload module {module_name}: {e}")
# Directly import required modules, not dependent on loader.py
try:
# Try to import server module
om.MGlobal.displayInfo("Attempting to import server module...")
# Add current directory to sys.path
# 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 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}")
if current_dir not in sys.path:
sys.path.insert(0, current_dir)
om.MGlobal.displayInfo(f"Added directory to sys.path: {current_dir}")
# Import server module
import server
om.MGlobal.displayInfo("Successfully imported server module")
# Force create menu
om.MGlobal.displayInfo("Forcing menu creation...")
menu_result = create_menu()
if menu_result:
om.MGlobal.displayInfo("Menu created successfully")
else:
om.MGlobal.displayWarning("Menu creation may have failed")
# Try alternative method to create menu
try:
# Use a deferred command to create the menu
# This gives Maya time to initialize the UI
cmds.evalDeferred("import maya_mcp_plugin; maya_mcp_plugin.create_menu()")
om.MGlobal.displayInfo("Scheduled deferred menu creation")
except Exception as e:
om.MGlobal.displayError(f"Failed to schedule deferred menu creation: {e}")
# Start server
port = server.start_server()
if port:
message = "=" * 50
message += "\nMCP SERVER STARTED SUCCESSFULLY ON PORT {}\n".format(port)
message += "=" * 50
om.MGlobal.displayInfo(message)
else:
om.MGlobal.displayError("Failed to start MCP server")
except Exception as e:
om.MGlobal.displayError(f"Failed to load MCP server: {e}")
om.MGlobal.displayError(f"Traceback: {traceback.format_exc()}")
except Exception as e:
om.MGlobal.displayError(f"Error initializing MCP plugin: {e}")
raise
# Plugin cleanup
def uninitializePlugin(plugin):
"""
Uninitialize the plugin
Args:
plugin: MObject used to deregister commands
"""
try:
# Stop MCP server
try:
from server import stop_server
stop_server()
om.MGlobal.displayInfo("MCP server stopped")
except Exception as e:
om.MGlobal.displayWarning(f"Failed to stop MCP server: {e}")
# Remove menu - Try to use a more forceful way to delete menus
try:
# Try multiple possible menu names
for menu_name in ["MCPMenu", "MCP", "MCPMainMenu"]:
if cmds.menu(menu_name, exists=True):
cmds.deleteUI(menu_name, menu=True)
om.MGlobal.displayInfo(f"MCP menu '{menu_name}' removed")
# Check all menus in the main window, find menus containing MCP
main_window = "MayaWindow"
if cmds.window(main_window, exists=True):
menus = cmds.window(main_window, query=True, menuArray=True) or []
for menu in menus:
menu_label = cmds.menu(menu, query=True, label=True)
if menu_label and "MCP" in menu_label:
cmds.deleteUI(menu, menu=True)
om.MGlobal.displayInfo(f"Found and removed MCP menu with label: {menu_label}")
except Exception as e:
om.MGlobal.displayWarning(f"Failed to remove MCP menu: {e}")
except Exception as e:
om.MGlobal.displayError(f"Error uninitializing MCP plugin: {e}")
raise