Skip to main content
Glama
mcp_server.py23.5 kB
#!/usr/bin/env python3 """ MCP Server для работы с проектами GameMaker Studio 2 Предоставляет инструменты для парсинга и анализа GMS2 проектов """ import asyncio import argparse import json import os import sys from typing import Any, Dict, List, Optional from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent from dotenv import load_dotenv # Импортируем gms2_parser из локальной папки from gms2_parser import GMS2ProjectParser class GMS2MCPServer: """MCP Сервер для работы с GameMaker Studio 2""" def __init__(self, project_path: Optional[str] = None): self.project_path = project_path self.parser = None if project_path: self.parser = GMS2ProjectParser(project_path) print(f"DEBUG: GMS2MCPServer initialized with project_path: {project_path}") def _get_project_path(self, arguments: Dict[str, Any]) -> str: """Получает правильный путь к проекту из аргументов или config.env""" provided_path = arguments.get("project_path") # Если путь не передан, используем из config.env if not provided_path: if self.project_path: return self.project_path else: # Попытаемся загрузить из config.env напрямую config_file = os.path.join(os.path.dirname(__file__), 'config.env') print(f"DEBUG: Looking for config.env at: {config_file}") load_dotenv(config_file) config_path = os.getenv('GMS2_PROJECT_PATH') if config_path: print(f"DEBUG: Loading project path from config.env: {config_path}") return config_path else: raise ValueError(f"Project path not configured in config.env and not provided. Current self.project_path: {self.project_path}, config_file: {config_file}") # Проверяем, не является ли переданный путь корнем MCP сервера current_dir = os.getcwd() if os.path.abspath(provided_path) == os.path.abspath(current_dir): if self.project_path: return self.project_path else: # Попытаемся загрузить из config.env напрямую config_file = os.path.join(os.path.dirname(__file__), 'config.env') print(f"DEBUG: Looking for config.env at: {config_file}") load_dotenv(config_file) config_path = os.getenv('GMS2_PROJECT_PATH') if config_path: print(f"DEBUG: Loading project path from config.env: {config_path}") return config_path else: raise ValueError(f"Provided path is MCP server root, but project path not configured in config.env. Current self.project_path: {self.project_path}, config_file: {config_file}") # Если передан другой путь, используем его return provided_path def get_tools(self) -> List[Tool]: """Возвращает список доступных инструментов""" return [ Tool( name="scan_gms2_project", description="Сканирует проект GameMaker Studio 2 и возвращает структуру файлов", inputSchema={ "type": "object", "properties": { "project_path": { "type": "string", "description": "Путь к папке проекта GMS2 (необязательно, используется из config.env)" } }, "required": [] } ), Tool( name="get_gml_file_content", description="Получает содержимое конкретного GML файла", inputSchema={ "type": "object", "properties": { "project_path": { "type": "string", "description": "Путь к папке проекта GMS2 (необязательно, используется из config.env)" }, "file_path": { "type": "string", "description": "Путь к GML файлу (относительный или абсолютный)" } }, "required": ["file_path"] } ), Tool( name="get_room_info", description="Получает детальную информацию о комнате из .yy файла", inputSchema={ "type": "object", "properties": { "project_path": { "type": "string", "description": "Путь к папке проекта GMS2 (необязательно, используется из config.env)" }, "room_name": { "type": "string", "description": "Имя комнаты" } }, "required": ["room_name"] } ), Tool( name="get_object_info", description="Получает детальную информацию об объекте из .yy файла", inputSchema={ "type": "object", "properties": { "project_path": { "type": "string", "description": "Путь к папке проекта GMS2 (необязательно, используется из config.env)" }, "object_name": { "type": "string", "description": "Имя объекта" } }, "required": ["object_name"] } ), Tool( name="get_sprite_info", description="Получает информацию о спрайте", inputSchema={ "type": "object", "properties": { "project_path": { "type": "string", "description": "Путь к папке проекта GMS2 (необязательно, используется из config.env)" }, "sprite_name": { "type": "string", "description": "Имя спрайта" } }, "required": ["sprite_name"] } ), Tool( name="export_project_data", description="Экспортирует все данные проекта в текстовый формат (аналог функции из vibe2gml)", inputSchema={ "type": "object", "properties": { "project_path": { "type": "string", "description": "Путь к папке проекта GMS2 (необязательно, используется из config.env)" }, "save_to_file": { "type": "boolean", "description": "Сохранить результат в файл (по умолчанию false)", "default": False }, "output_file": { "type": "string", "description": "Путь для сохранения файла (если save_to_file=true)" } }, "required": [] } ), Tool( name="list_project_assets", description="Получает список всех ассетов проекта по категориям", inputSchema={ "type": "object", "properties": { "project_path": { "type": "string", "description": "Путь к папке проекта GMS2 (необязательно, используется из config.env)" }, "category": { "type": "string", "description": "Фильтр по категории (Objects, Scripts, Rooms, Sprites, etc.)", "enum": ["Objects", "Scripts", "Rooms", "Sprites", "Notes", "Tile Sets", "Timelines", "Fonts", "Sounds", "Extensions"] } }, "required": [] } ) ] async def handle_tool_call(self, name: str, arguments: Dict[str, Any]) -> List[TextContent]: """Обрабатывает вызовы инструментов""" try: if name == "scan_gms2_project": return await self._scan_project(arguments) elif name == "get_gml_file_content": return await self._get_gml_content(arguments) elif name == "get_room_info": return await self._get_room_info(arguments) elif name == "get_object_info": return await self._get_object_info(arguments) elif name == "get_sprite_info": return await self._get_sprite_info(arguments) elif name == "export_project_data": return await self._export_project_data(arguments) elif name == "list_project_assets": return await self._list_project_assets(arguments) else: return [TextContent(type="text", text=f"Unknown tool: {name}")] except Exception as e: return [TextContent(type="text", text=f"Error executing tool {name}: {str(e)}")] async def _scan_project(self, arguments: Dict[str, Any]) -> List[TextContent]: """Сканирует проект GMS2""" try: project_path = self._get_project_path(arguments) except ValueError as e: return [TextContent(type="text", text=f"Error: {str(e)}")] parser = GMS2ProjectParser(project_path) result = parser.scan_project() if "error" in result: return [TextContent(type="text", text=f"Error: {result['error']}")] # Форматируем результат для удобного чтения output = [] output.append(f"GameMaker Studio 2 Project: {result['project_name']}") output.append(f"Path: {result['project_path']}") output.append(f"Total GML Files: {result['total_gml_files']}") output.append("") # Показываем структуру по категориям for category, info in result['categories'].items(): if info['assets']: output.append(f"{category}: {len(info['assets'])} assets") for asset in info['assets']: gml_count = len(asset['gml_files']) yy_status = "✓" if asset['yy_file'] else "✗" output.append(f" - {asset['name']} (GML: {gml_count}, YY: {yy_status})") output.append("") output.append("Recent GML Files:") for i, (display_name, _, relative_path, _) in enumerate(result['gml_files'][:10]): output.append(f" {i+1}. {display_name} ({relative_path})") if len(result['gml_files']) > 10: output.append(f" ... and {len(result['gml_files']) - 10} more files") return [TextContent(type="text", text="\n".join(output))] async def _get_gml_content(self, arguments: Dict[str, Any]) -> List[TextContent]: """Получает содержимое GML файла""" try: project_path = self._get_project_path(arguments) except ValueError as e: return [TextContent(type="text", text=f"Error: {str(e)}")] file_path = arguments.get("file_path") if not file_path: return [TextContent(type="text", text="Error: file_path is required")] parser = GMS2ProjectParser(project_path) # Если путь относительный, делаем его абсолютным if not os.path.isabs(file_path): file_path = os.path.join(project_path, file_path) result = parser.get_gml_content(file_path) if "error" in result: return [TextContent(type="text", text=f"Error: {result['error']}")] output = [] output.append(f"GML File: {result['relative_path']}") output.append(f"Lines: {result['line_count']}") output.append("-" * 50) output.append(result['content']) return [TextContent(type="text", text="\n".join(output))] async def _get_room_info(self, arguments: Dict[str, Any]) -> List[TextContent]: """Получает информацию о комнате""" try: project_path = self._get_project_path(arguments) except ValueError as e: return [TextContent(type="text", text=f"Error: {str(e)}")] room_name = arguments.get("room_name") if not room_name: return [TextContent(type="text", text="Error: room_name is required")] parser = GMS2ProjectParser(project_path) result = parser.get_room_info(room_name) if "error" in result: return [TextContent(type="text", text=f"Error: {result['error']}")] output = [] output.append(f"Room Information: {result['room_name']}") output.append("=" * 50) output.append("") output.append("Formatted View:") output.append(result['formatted_info']) output.append("") output.append("Raw Data Available:") output.append(f"- YY File: {result['yy_path']}") output.append(f"- Layers: {len(result['data'].get('layers', []))}") output.append(f"- Room Settings: {'Yes' if result['data'].get('roomSettings') else 'No'}") return [TextContent(type="text", text="\n".join(output))] async def _get_object_info(self, arguments: Dict[str, Any]) -> List[TextContent]: """Получает информацию об объекте""" try: project_path = self._get_project_path(arguments) except ValueError as e: return [TextContent(type="text", text=f"Error: {str(e)}")] object_name = arguments.get("object_name") if not object_name: return [TextContent(type="text", text="Error: object_name is required")] parser = GMS2ProjectParser(project_path) result = parser.get_object_info(object_name) if "error" in result: return [TextContent(type="text", text=f"Error: {result['error']}")] output = [] output.append(f"Object Information: {result['object_name']}") output.append("=" * 50) output.append("") output.append("Formatted View:") output.append(result['formatted_info']) output.append("") output.append("Raw Data Available:") output.append(f"- YY File: {result['yy_path']}") output.append(f"- Events: {len(result['data'].get('eventList', []))}") output.append(f"- Physics: {'Enabled' if result['data'].get('physicsObject') else 'Disabled'}") return [TextContent(type="text", text="\n".join(output))] async def _get_sprite_info(self, arguments: Dict[str, Any]) -> List[TextContent]: """Получает информацию о спрайте""" try: project_path = self._get_project_path(arguments) except ValueError as e: return [TextContent(type="text", text=f"Error: {str(e)}")] sprite_name = arguments.get("sprite_name") if not sprite_name: return [TextContent(type="text", text="Error: sprite_name is required")] parser = GMS2ProjectParser(project_path) result = parser.get_sprite_info(sprite_name) if "error" in result: return [TextContent(type="text", text=f"Error: {result['error']}")] output = [] output.append(f"Sprite Information: {result['sprite_name']}") output.append("=" * 50) output.append("") output.append(f"Sprite Path: {result['sprite_path']}") output.append(f"YY File: {'Yes' if result['yy_path'] else 'No'}") output.append(f"Frame Count: {len(result['frames'])}") if result['frames']: output.append("") output.append("Frames:") for i, frame in enumerate(result['frames']): output.append(f" {i+1}. {frame['filename']}") return [TextContent(type="text", text="\n".join(output))] async def _export_project_data(self, arguments: Dict[str, Any]) -> List[TextContent]: """Экспортирует все данные проекта""" try: project_path = self._get_project_path(arguments) except ValueError as e: return [TextContent(type="text", text=f"Error: {str(e)}")] save_to_file = arguments.get("save_to_file", False) output_file = arguments.get("output_file") parser = GMS2ProjectParser(project_path) export_data = parser.export_all_data() if save_to_file: if not output_file: # Генерируем имя файла по умолчанию project_name = os.path.basename(project_path) output_file = f"{project_name}_export.txt" try: with open(output_file, 'w', encoding='utf-8') as f: f.write(export_data) return [TextContent(type="text", text=f"Project data exported to: {output_file}\n\nFile size: {len(export_data)} characters")] except Exception as e: return [TextContent(type="text", text=f"Error saving file: {str(e)}\n\nExport data:\n{export_data}")] else: # Возвращаем данные напрямую return [TextContent(type="text", text=export_data)] async def _list_project_assets(self, arguments: Dict[str, Any]) -> List[TextContent]: """Список ассетов проекта""" try: project_path = self._get_project_path(arguments) except ValueError as e: return [TextContent(type="text", text=f"Error: {str(e)}")] category_filter = arguments.get("category") parser = GMS2ProjectParser(project_path) result = parser.scan_project() if "error" in result: return [TextContent(type="text", text=f"Error: {result['error']}")] output = [] output.append(f"Assets in {result['project_name']}:") output.append("=" * 50) categories_to_show = [category_filter] if category_filter else result['categories'].keys() for category in categories_to_show: if category in result['categories']: info = result['categories'][category] if info['assets']: output.append(f"\n{category} ({len(info['assets'])} items):") for asset in info['assets']: gml_files = len(asset['gml_files']) yy_file = "✓" if asset['yy_file'] else "✗" output.append(f" - {asset['name']} (GML: {gml_files}, YY: {yy_file})") # Показываем GML файлы если их немного if gml_files > 0 and gml_files <= 5: for gml in asset['gml_files']: output.append(f" • {gml['name']}") return [TextContent(type="text", text="\n".join(output))] async def main(): """Главная функция запуска сервера""" # Загружаем переменные окружения из config.env config_file = os.path.join(os.path.dirname(__file__), 'config.env') load_dotenv(config_file) parser = argparse.ArgumentParser(description="GameMaker Studio 2 MCP Server") parser.add_argument("--project-path", type=str, help="Path to GMS2 project (overrides config.env)") args = parser.parse_args() # Определяем путь к проекту: командная строка > config.env > None project_path = args.project_path or os.getenv('GMS2_PROJECT_PATH') if project_path and not os.path.exists(project_path): print(f"Warning: Project path does not exist: {project_path}") # Создаем экземпляр сервера mcp_server = GMS2MCPServer(project_path) # Отладочная информация if project_path: print(f"MCP Server initialized with project path: {project_path}") # Создаем MCP Server server = Server("gms2-mcp-server") # Регистрируем список инструментов @server.list_tools() async def handle_list_tools() -> list[Tool]: return mcp_server.get_tools() # Регистрируем обработчик вызовов инструментов @server.call_tool() async def handle_call_tool(name: str, arguments: dict) -> list[TextContent]: return await mcp_server.handle_tool_call(name, arguments) # Запускаем сервер через stdio async with stdio_server() as (read_stream, write_stream): await server.run(read_stream, write_stream, server.create_initialization_options()) if __name__ == "__main__": try: print("Starting MCP server...", file=sys.stderr) asyncio.run(main()) except Exception as e: print(f"Error starting MCP server: {e}", file=sys.stderr) import traceback traceback.print_exc() sys.exit(1)

Latest Blog Posts

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/Atennebris/gms2-mcp-server'

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