Skip to main content
Glama

Xbox Controller MCP Server

by bcperry
main.py14.7 kB
#!/usr/bin/env python3 import asyncio import time from typing import Optional, Dict, Any from contextlib import asynccontextmanager try: import vgamepad as vg VGAMEPAD_AVAILABLE = True except Exception as e: VGAMEPAD_AVAILABLE = False VGAMEPAD_ERROR = str(e) from mcp.server.fastmcp import FastMCP from pydantic import BaseModel class ControllerState(BaseModel): """Represents the current state of the Xbox controller""" left_stick_x: float = 0.0 # -1.0 to 1.0 left_stick_y: float = 0.0 # -1.0 to 1.0 right_stick_x: float = 0.0 # -1.0 to 1.0 right_stick_y: float = 0.0 # -1.0 to 1.0 left_trigger: float = 0.0 # 0.0 to 1.0 right_trigger: float = 0.0 # 0.0 to 1.0 buttons_pressed: Dict[str, bool] = {} class XboxControllerEmulator: """Xbox controller emulator with fallback simulation mode""" def __init__(self): self.state = ControllerState() self.simulation_mode = not VGAMEPAD_AVAILABLE if not self.simulation_mode: try: self.gamepad = vg.VX360Gamepad() except Exception as e: print(f"Warning: Failed to initialize virtual gamepad: {e}") print("Falling back to simulation mode.") self.simulation_mode = True self.gamepad = None else: self.gamepad = None print(f"Warning: vgamepad not available: {VGAMEPAD_ERROR if VGAMEPAD_AVAILABLE else 'Module not found'}") print("Running in simulation mode - controller state will be tracked but no actual input will be sent.") self.button_map = { 'A': 'XUSB_GAMEPAD_A', 'B': 'XUSB_GAMEPAD_B', 'X': 'XUSB_GAMEPAD_X', 'Y': 'XUSB_GAMEPAD_Y', 'LB': 'XUSB_GAMEPAD_LEFT_SHOULDER', 'RB': 'XUSB_GAMEPAD_RIGHT_SHOULDER', 'BACK': 'XUSB_GAMEPAD_BACK', 'START': 'XUSB_GAMEPAD_START', 'LS': 'XUSB_GAMEPAD_LEFT_THUMB', 'RS': 'XUSB_GAMEPAD_RIGHT_THUMB', 'DPAD_UP': 'XUSB_GAMEPAD_DPAD_UP', 'DPAD_DOWN': 'XUSB_GAMEPAD_DPAD_DOWN', 'DPAD_LEFT': 'XUSB_GAMEPAD_DPAD_LEFT', 'DPAD_RIGHT': 'XUSB_GAMEPAD_DPAD_RIGHT', } # Initialize button states for button in self.button_map.keys(): self.state.buttons_pressed[button] = False def _log_action(self, action: str, details: str = ""): """Log controller actions for debugging/simulation""" mode_prefix = "[SIM]" if self.simulation_mode else "[HW]" print(f"{mode_prefix} {action}{f' - {details}' if details else ''}") def press_button(self, button: str) -> bool: """Press a button on the controller""" if button not in self.button_map: return False self.state.buttons_pressed[button] = True self._log_action(f"Press {button}") if not self.simulation_mode and self.gamepad: try: vg_button = getattr(vg.XUSB_BUTTON, self.button_map[button]) self.gamepad.press_button(button=vg_button) self.gamepad.update() except Exception as e: self._log_action(f"Error pressing {button}", str(e)) return True def release_button(self, button: str) -> bool: """Release a button on the controller""" if button not in self.button_map: return False self.state.buttons_pressed[button] = False self._log_action(f"Release {button}") if not self.simulation_mode and self.gamepad: try: vg_button = getattr(vg.XUSB_BUTTON, self.button_map[button]) self.gamepad.release_button(button=vg_button) self.gamepad.update() except Exception as e: self._log_action(f"Error releasing {button}", str(e)) return True def tap_button(self, button: str, duration: float = 0.1) -> bool: """Tap a button (press and release after duration)""" if self.press_button(button): time.sleep(duration) return self.release_button(button) return False def set_left_stick(self, x: float, y: float): """Set left stick position (-1.0 to 1.0 for both axes)""" x = max(-1.0, min(1.0, x)) y = max(-1.0, min(1.0, y)) self.state.left_stick_x = x self.state.left_stick_y = y self._log_action(f"Left stick", f"x={x:.2f}, y={y:.2f}") if not self.simulation_mode and self.gamepad: try: x_value = int(x * 32767) y_value = int(y * 32767) self.gamepad.left_joystick(x_value=x_value, y_value=y_value) self.gamepad.update() except Exception as e: self._log_action(f"Error setting left stick", str(e)) def set_right_stick(self, x: float, y: float): """Set right stick position (-1.0 to 1.0 for both axes)""" x = max(-1.0, min(1.0, x)) y = max(-1.0, min(1.0, y)) self.state.right_stick_x = x self.state.right_stick_y = y self._log_action(f"Right stick", f"x={x:.2f}, y={y:.2f}") if not self.simulation_mode and self.gamepad: try: x_value = int(x * 32767) y_value = int(y * 32767) self.gamepad.right_joystick(x_value=x_value, y_value=y_value) self.gamepad.update() except Exception as e: self._log_action(f"Error setting right stick", str(e)) def set_triggers(self, left: float, right: float): """Set trigger values (0.0 to 1.0)""" left = max(0.0, min(1.0, left)) right = max(0.0, min(1.0, right)) self.state.left_trigger = left self.state.right_trigger = right self._log_action(f"Triggers", f"left={left:.2f}, right={right:.2f}") if not self.simulation_mode and self.gamepad: try: left_value = int(left * 255) right_value = int(right * 255) self.gamepad.left_trigger(value=left_value) self.gamepad.right_trigger(value=right_value) self.gamepad.update() except Exception as e: self._log_action(f"Error setting triggers", str(e)) def reset_controller(self): """Reset controller to neutral state""" self._log_action("Reset controller") if not self.simulation_mode and self.gamepad: try: self.gamepad.reset() self.gamepad.update() except Exception as e: self._log_action(f"Error resetting controller", str(e)) # Reset state regardless of hardware mode self.state = ControllerState() for button in self.button_map.keys(): self.state.buttons_pressed[button] = False def get_state(self) -> Dict[str, Any]: """Get current controller state""" state_dict = self.state.model_dump() state_dict['simulation_mode'] = self.simulation_mode return state_dict # Global controller instance controller = XboxControllerEmulator() # Create FastMCP app mcp = FastMCP("Xbox Controller Emulator") @mcp.tool() def press_button(button: str) -> Dict[str, Any]: """ Press a button on the Xbox controller. Args: button: Button name (A, B, X, Y, LB, RB, BACK, START, LS, RS, DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT) Returns: Dict with success status and current button states """ success = controller.press_button(button) return { "success": success, "button": button, "action": "pressed", "current_state": controller.get_state() } @mcp.tool() def release_button(button: str) -> Dict[str, Any]: """ Release a button on the Xbox controller. Args: button: Button name (A, B, X, Y, LB, RB, BACK, START, LS, RS, DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT) Returns: Dict with success status and current button states """ success = controller.release_button(button) return { "success": success, "button": button, "action": "released", "current_state": controller.get_state() } @mcp.tool() def tap_button(button: str, duration: float = 0.1) -> Dict[str, Any]: """ Tap a button on the Xbox controller (press and release). Args: button: Button name (A, B, X, Y, LB, RB, BACK, START, LS, RS, DPAD_UP, DPAD_DOWN, DPAD_LEFT, DPAD_RIGHT) duration: How long to hold the button in seconds (default: 0.1) Returns: Dict with success status and action performed """ success = controller.tap_button(button, duration) return { "success": success, "button": button, "action": "tapped", "duration": duration, "current_state": controller.get_state() } @mcp.tool() def set_left_stick(x: float, y: float) -> Dict[str, Any]: """ Set the left analog stick position. Args: x: X-axis position (-1.0 to 1.0, left to right) y: Y-axis position (-1.0 to 1.0, down to up) Returns: Dict with success status and current stick position """ # Clamp values to valid range x = max(-1.0, min(1.0, x)) y = max(-1.0, min(1.0, y)) controller.set_left_stick(x, y) return { "success": True, "stick": "left", "x": x, "y": y, "current_state": controller.get_state() } @mcp.tool() def set_right_stick(x: float, y: float) -> Dict[str, Any]: """ Set the right analog stick position. Args: x: X-axis position (-1.0 to 1.0, left to right) y: Y-axis position (-1.0 to 1.0, down to up) Returns: Dict with success status and current stick position """ # Clamp values to valid range x = max(-1.0, min(1.0, x)) y = max(-1.0, min(1.0, y)) controller.set_right_stick(x, y) return { "success": True, "stick": "right", "x": x, "y": y, "current_state": controller.get_state() } @mcp.tool() def set_triggers(left: float, right: float) -> Dict[str, Any]: """ Set the trigger values. Args: left: Left trigger value (0.0 to 1.0) right: Right trigger value (0.0 to 1.0) Returns: Dict with success status and current trigger values """ # Clamp values to valid range left = max(0.0, min(1.0, left)) right = max(0.0, min(1.0, right)) controller.set_triggers(left, right) return { "success": True, "left_trigger": left, "right_trigger": right, "current_state": controller.get_state() } @mcp.tool() def reset_controller() -> Dict[str, Any]: """ Reset the controller to neutral state (all inputs released). Returns: Dict with success status and reset state """ controller.reset_controller() return { "success": True, "action": "reset", "current_state": controller.get_state() } @mcp.tool() def get_controller_state() -> Dict[str, Any]: """ Get the current state of the Xbox controller. Returns: Dict with current controller state including sticks, triggers, and buttons """ return { "success": True, "current_state": controller.get_state() } @mcp.tool() def list_available_buttons() -> Dict[str, Any]: """ List all available button names that can be used with the controller. Returns: Dict with list of available button names """ return { "success": True, "available_buttons": list(controller.button_map.keys()), "button_descriptions": { "A": "A button (bottom face button)", "B": "B button (right face button)", "X": "X button (left face button)", "Y": "Y button (top face button)", "LB": "Left bumper", "RB": "Right bumper", "BACK": "Back/Select button", "START": "Start/Menu button", "LS": "Left stick click", "RS": "Right stick click", "DPAD_UP": "D-pad up", "DPAD_DOWN": "D-pad down", "DPAD_LEFT": "D-pad left", "DPAD_RIGHT": "D-pad right" } } @mcp.tool() def get_system_info() -> Dict[str, Any]: """ Get information about the controller emulation system. Returns: Dict with system information and setup status """ return { "success": True, "vgamepad_available": VGAMEPAD_AVAILABLE, "simulation_mode": controller.simulation_mode, "vgamepad_error": VGAMEPAD_ERROR if not VGAMEPAD_AVAILABLE else None, "setup_instructions": { "windows": "Install ViGEmBus driver from https://github.com/ViGEm/ViGEmBus/releases", "note": "Without ViGEmBus, the server runs in simulation mode (state tracking only)" } } if __name__ == "__main__": """Run the MCP server""" print("Starting Xbox Controller MCP Server...") print("Available tools:") print("- press_button: Press a controller button") print("- release_button: Release a controller button") print("- tap_button: Tap a controller button") print("- set_left_stick: Set left analog stick position") print("- set_right_stick: Set right analog stick position") print("- set_triggers: Set trigger values") print("- reset_controller: Reset controller to neutral state") print("- get_controller_state: Get current controller state") print("- list_available_buttons: List all available buttons") print("- get_system_info: Get system information and setup status") print() if controller.simulation_mode: print("⚠️ Running in SIMULATION MODE") print(" Controller state will be tracked but no actual input will be sent.") print(" To enable hardware mode, install ViGEmBus:") print(" https://github.com/ViGEm/ViGEmBus/releases") print() else: print("✅ Hardware mode enabled - virtual controller ready!") print() print("Controller emulator initialized and ready!") # Run the MCP server mcp.run(transport="streamable-http")

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bcperry/controller_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server