555 lines
21 KiB
Python
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
|