MergeBalanceTools / mcp_server.py
aigurletov's picture
mcp
18cbfa9
"""
MCP Server для интеграции симулятора заказов с ИИ агентами
Предоставляет инструменты для автоматизированной работы с симулятором
"""
import json
import pandas as pd
from typing import Dict, List, Any, Optional, Union
from dataclasses import asdict
import tempfile
import os
from config_manager import ConfigManager, SimulatorConfig, create_config_from_ui_state, apply_config_to_ui
class MergeSimulatorMCPServer:
"""MCP сервер для симулятора генерации заказов"""
def __init__(self):
self.config_manager = ConfigManager()
self.current_simulation_results = None
self.current_stats_report = ""
# ===============================================================
# CONFIG MANAGEMENT TOOLS
# ===============================================================
def mcp_save_simulator_config(
self,
name: str,
description: str,
chain_data: List[Dict[str, Any]] = None,
energy_rewards_data: List[Dict[str, Any]] = None,
item_rewards_data: List[Dict[str, Any]] = None,
max_history_orders: int = 5,
increment_difficulty: int = 2,
energy_chance: int = 90,
requirement_weights: str = "70,30",
reduction_factor: int = 3,
increase_factor: int = 5,
iteration_count: int = 100,
initial_energy: int = 10000
) -> Dict[str, Any]:
"""
Сохраняет конфигурацию симулятора для последующего использования
Args:
name: Название конфигурации
description: Описание конфигурации
chain_data: Данные цепочек merge-предметов
energy_rewards_data: Данные энергетических наград
item_rewards_data: Данные предметных наград
max_history_orders: Максимальное количество заказов в истории
increment_difficulty: Инкремент сложности
energy_chance: Шанс выпадения энергетической награды (%)
requirement_weights: Веса требований (строка, например "70,30")
reduction_factor: Фактор уменьшения
increase_factor: Фактор увеличения
iteration_count: Количество итераций симуляции
initial_energy: Начальное количество энергии
Returns:
Словарь с результатом операции и путем к сохраненному файлу
"""
try:
config = SimulatorConfig(
name=name,
description=description,
created_at=pd.Timestamp.now().isoformat(),
max_history_orders=max_history_orders,
increment_difficulty=increment_difficulty,
energy_chance=energy_chance,
requirement_weights=requirement_weights,
reduction_factor=reduction_factor,
increase_factor=increase_factor,
iteration_count=iteration_count,
initial_energy=initial_energy,
chain_data=chain_data or [],
energy_rewards_data=energy_rewards_data or [],
item_rewards_data=item_rewards_data or []
)
filepath = self.config_manager.save_config(config)
return {
"success": True,
"message": f"Конфигурация '{name}' успешно сохранена",
"filepath": filepath,
"config_name": name
}
except Exception as e:
return {
"success": False,
"error": str(e),
"message": f"Ошибка при сохранении конфигурации: {str(e)}"
}
def mcp_load_simulator_config(self, config_name_or_path: str) -> Dict[str, Any]:
"""
Загружает конфигурацию симулятора
Args:
config_name_or_path: Название конфигурации или путь к файлу
Returns:
Словарь с данными конфигурации
"""
try:
# Если передан путь к файлу
if config_name_or_path.endswith('.json') and os.path.exists(config_name_or_path):
config = self.config_manager.load_config(config_name_or_path)
else:
# Ищем конфигурацию по имени
configs = self.config_manager.list_configs()
matching_config = next((c for c in configs if c['name'] == config_name_or_path), None)
if not matching_config:
return {
"success": False,
"error": f"Конфигурация '{config_name_or_path}' не найдена",
"available_configs": [c['name'] for c in configs]
}
config = self.config_manager.load_config(matching_config['filepath'])
return {
"success": True,
"config": config.to_dict(),
"message": f"Конфигурация '{config.name}' успешно загружена"
}
except Exception as e:
return {
"success": False,
"error": str(e),
"message": f"Ошибка при загрузке конфигурации: {str(e)}"
}
def mcp_list_simulator_configs(self) -> Dict[str, Any]:
"""
Возвращает список доступных конфигураций симулятора
Returns:
Словарь со списком конфигураций
"""
try:
configs = self.config_manager.list_configs()
return {
"success": True,
"configs": configs,
"count": len(configs),
"message": f"Найдено {len(configs)} конфигураций"
}
except Exception as e:
return {
"success": False,
"error": str(e),
"message": f"Ошибка при получении списка конфигураций: {str(e)}"
}
def mcp_delete_simulator_config(self, config_name_or_path: str) -> Dict[str, Any]:
"""
Удаляет конфигурацию симулятора
Args:
config_name_or_path: Название конфигурации или путь к файлу
Returns:
Словарь с результатом операции
"""
try:
# Если передан путь к файлу
if config_name_or_path.endswith('.json') and os.path.exists(config_name_or_path):
filepath = config_name_or_path
else:
# Ищем конфигурацию по имени
configs = self.config_manager.list_configs()
matching_config = next((c for c in configs if c['name'] == config_name_or_path), None)
if not matching_config:
return {
"success": False,
"error": f"Конфигурация '{config_name_or_path}' не найдена"
}
filepath = matching_config['filepath']
success = self.config_manager.delete_config(filepath)
if success:
return {
"success": True,
"message": f"Конфигурация успешно удалена"
}
else:
return {
"success": False,
"error": "Не удалось удалить конфигурацию"
}
except Exception as e:
return {
"success": False,
"error": str(e),
"message": f"Ошибка при удалении конфигурации: {str(e)}"
}
# ===============================================================
# SIMULATION TOOLS
# ===============================================================
def mcp_run_simulation(
self,
config_name_or_data: Union[str, Dict[str, Any]] = None,
chain_data: List[Dict[str, Any]] = None,
energy_rewards_data: List[Dict[str, Any]] = None,
item_rewards_data: List[Dict[str, Any]] = None,
max_history_orders: int = None,
increment_difficulty: int = None,
energy_chance: int = None,
requirement_weights: str = None,
reduction_factor: int = None,
increase_factor: int = None,
iteration_count: int = None,
initial_energy: int = None,
return_detailed_results: bool = True
) -> Dict[str, Any]:
"""
Запускает симуляцию генерации заказов
Args:
config_name_or_data: Название конфигурации или данные конфигурации
chain_data: Данные цепочек (переопределяет данные из конфигурации)
energy_rewards_data: Данные энергетических наград
item_rewards_data: Данные предметных наград
max_history_orders: Максимальное количество заказов в истории
increment_difficulty: Инкремент сложности
energy_chance: Шанс выпадения энергетической награды (%)
requirement_weights: Веса требований
reduction_factor: Фактор уменьшения
increase_factor: Фактор увеличения
iteration_count: Количество итераций симуляции
initial_energy: Начальное количество энергии
return_detailed_results: Возвращать ли детальные результаты
Returns:
Словарь с результатами симуляции
"""
try:
# Импортируем функцию симуляции
from app import run_simulation_interface
# Загружаем конфигурацию если указано имя
if isinstance(config_name_or_data, str):
config_result = self.mcp_load_simulator_config(config_name_or_data)
if not config_result["success"]:
return config_result
config_data = config_result["config"]
elif isinstance(config_name_or_data, dict):
config_data = config_name_or_data
else:
config_data = {}
# Применяем параметры (переданные параметры имеют приоритет над конфигурацией)
params = {
'max_history': max_history_orders or config_data.get('max_history_orders', 5),
'increment_diff': increment_difficulty or config_data.get('increment_difficulty', 2),
'energy_chance': energy_chance or config_data.get('energy_chance', 90),
'req_weights': requirement_weights or config_data.get('requirement_weights', "70,30"),
'reduction_factor': reduction_factor or config_data.get('reduction_factor', 3),
'increase_factor': increase_factor or config_data.get('increase_factor', 5),
'iteration_count': iteration_count or config_data.get('iteration_count', 100),
'initial_energy': initial_energy or config_data.get('initial_energy', 10000)
}
# Подготавливаем данные
chain_df = pd.DataFrame(chain_data or config_data.get('chain_data', []))
energy_rewards_df = pd.DataFrame(energy_rewards_data or config_data.get('energy_rewards_data', []))
item_rewards_df = pd.DataFrame(item_rewards_data or config_data.get('item_rewards_data', []))
# Запускаем симуляцию
results_df, stats_report, _, _ = run_simulation_interface(
chain_df, energy_rewards_df, item_rewards_df,
params['max_history'], params['increment_diff'], params['energy_chance'],
params['req_weights'], params['reduction_factor'], params['increase_factor'],
params['iteration_count'], params['initial_energy']
)
# Сохраняем результаты
self.current_simulation_results = results_df
self.current_stats_report = stats_report
response = {
"success": True,
"message": "Симуляция завершена успешно",
"stats_report": stats_report,
"total_orders": len(results_df),
"simulation_parameters": params
}
if return_detailed_results:
response["detailed_results"] = results_df.to_dict('records')
return response
except Exception as e:
return {
"success": False,
"error": str(e),
"message": f"Ошибка при выполнении симуляции: {str(e)}"
}
def mcp_get_simulation_results(self, format: str = "summary") -> Dict[str, Any]:
"""
Получает результаты последней симуляции
Args:
format: Формат результатов ("summary", "detailed", "csv")
Returns:
Словарь с результатами симуляции
"""
try:
if self.current_simulation_results is None:
return {
"success": False,
"error": "Нет доступных результатов симуляции",
"message": "Сначала запустите симуляцию"
}
if format == "summary":
return {
"success": True,
"stats_report": self.current_stats_report,
"total_orders": len(self.current_simulation_results),
"avg_difficulty": self.current_simulation_results['Total_Difficulty'].mean(),
"final_energy": self.current_simulation_results['MEnergy_Amount'].iloc[-1] if len(self.current_simulation_results) > 0 else 0
}
elif format == "detailed":
return {
"success": True,
"stats_report": self.current_stats_report,
"detailed_results": self.current_simulation_results.to_dict('records')
}
elif format == "csv":
# Создаем временный CSV файл
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.csv', encoding='utf-8') as tmp_file:
self.current_simulation_results.to_csv(tmp_file.name, index=False)
csv_path = tmp_file.name
return {
"success": True,
"csv_file_path": csv_path,
"stats_report": self.current_stats_report
}
else:
return {
"success": False,
"error": f"Неизвестный формат: {format}",
"available_formats": ["summary", "detailed", "csv"]
}
except Exception as e:
return {
"success": False,
"error": str(e),
"message": f"Ошибка при получении результатов: {str(e)}"
}
def mcp_analyze_simulation_results(self, analysis_type: str = "basic") -> Dict[str, Any]:
"""
Анализирует результаты симуляции
Args:
analysis_type: Тип анализа ("basic", "detailed", "chains", "rewards")
Returns:
Словарь с результатами анализа
"""
try:
if self.current_simulation_results is None:
return {
"success": False,
"error": "Нет доступных результатов симуляции",
"message": "Сначала запустите симуляцию"
}
df = self.current_simulation_results
if analysis_type == "basic":
return {
"success": True,
"analysis": {
"total_orders": len(df),
"avg_difficulty": float(df['Total_Difficulty'].mean()),
"min_difficulty": int(df['Total_Difficulty'].min()),
"max_difficulty": int(df['Total_Difficulty'].max()),
"avg_energy_cost": float(df['MergeEnergyPrice'].mean()),
"total_energy_spent": int(df['MergeEnergyPrice'].sum()),
"final_energy": int(df['MEnergy_Amount'].iloc[-1]) if len(df) > 0 else 0,
"energy_rewards_count": int((df['ExpeditionEnergyReward'] > 0).sum()),
"item_rewards_count": int((df['MergeItemReward'] != "").sum())
}
}
elif analysis_type == "chains":
# Анализ по цепочкам
chain_analysis = {}
for col in df.columns:
if col.startswith('ChainId_'):
chain_counts = df[col].value_counts()
chain_analysis.update(chain_counts.to_dict())
return {
"success": True,
"chain_usage": chain_analysis,
"most_used_chain": max(chain_analysis.items(), key=lambda x: x[1]) if chain_analysis else None
}
elif analysis_type == "rewards":
# Анализ наград
energy_rewards = df[df['ExpeditionEnergyReward'] > 0]
item_rewards = df[df['MergeItemReward'] != ""]
return {
"success": True,
"rewards_analysis": {
"energy_rewards": {
"count": len(energy_rewards),
"total_amount": int(energy_rewards['ExpeditionEnergyReward'].sum()),
"avg_amount": float(energy_rewards['ExpeditionEnergyReward'].mean()) if len(energy_rewards) > 0 else 0
},
"item_rewards": {
"count": len(item_rewards),
"unique_items": item_rewards['MergeItemReward'].nunique(),
"most_common_item": item_rewards['MergeItemReward'].mode().iloc[0] if len(item_rewards) > 0 else None
}
}
}
else:
return {
"success": False,
"error": f"Неизвестный тип анализа: {analysis_type}",
"available_types": ["basic", "chains", "rewards"]
}
except Exception as e:
return {
"success": False,
"error": str(e),
"message": f"Ошибка при анализе результатов: {str(e)}"
}
# ===============================================================
# UTILITY TOOLS
# ===============================================================
def mcp_create_chain_data_template(self) -> Dict[str, Any]:
"""
Создает шаблон данных для цепочек merge-предметов
Returns:
Словарь с шаблоном данных цепочек
"""
template = [
{
"ChainId": "example_chain",
"MergeItemId": "item_level_1",
"RequirementWeight": 100,
"RewardDifficulty": 10
},
{
"ChainId": "example_chain",
"MergeItemId": "item_level_2",
"RequirementWeight": 80,
"RewardDifficulty": 25
},
{
"ChainId": "example_chain",
"MergeItemId": "item_level_3",
"RequirementWeight": 60,
"RewardDifficulty": 50
}
]
return {
"success": True,
"template": template,
"description": "Шаблон данных цепочек. ChainId - идентификатор цепочки, MergeItemId - идентификатор предмета, RequirementWeight - вес для генерации требований, RewardDifficulty - сложность для расчета наград"
}
def mcp_create_rewards_data_template(self) -> Dict[str, Any]:
"""
Создает шаблоны данных для наград
Returns:
Словарь с шаблонами данных наград
"""
energy_template = [
{"DifficultyScore": 100, "Amount": 1},
{"DifficultyScore": 500, "Amount": 3},
{"DifficultyScore": 1000, "Amount": 5}
]
item_template = [
{
"DifficultyScore": 200,
"Amount": 1,
"MergeItemId": "energy_1",
"RewardWeight": 80,
"ReductionFactor": 5
},
{
"DifficultyScore": 500,
"Amount": 1,
"MergeItemId": "coins_1",
"RewardWeight": 20,
"ReductionFactor": 0
}
]
return {
"success": True,
"energy_rewards_template": energy_template,
"item_rewards_template": item_template,
"description": "Шаблоны данных наград. DifficultyScore - порог сложности, Amount - количество награды, MergeItemId - ID предмета для предметных наград, RewardWeight - вес награды, ReductionFactor - фактор уменьшения"
}
def mcp_validate_config_data(self, config_data: Dict[str, Any]) -> Dict[str, Any]:
"""
Валидирует данные конфигурации симулятора
Args:
config_data: Данные конфигурации для валидации
Returns:
Словарь с результатами валидации
"""
try:
errors = []
warnings = []
# Проверяем обязательные поля
required_fields = ['name', 'description']
for field in required_fields:
if not config_data.get(field):
errors.append(f"Отсутствует обязательное поле: {field}")
# Проверяем числовые параметры
numeric_params = {
'max_history_orders': (1, 20),
'increment_difficulty': (0, 10),
'energy_chance': (0, 100),
'reduction_factor': (0, 100),
'increase_factor': (0, 100),
'iteration_count': (1, 10000),
'initial_energy': (1, 1000000)
}
for param, (min_val, max_val) in numeric_params.items():
value = config_data.get(param)
if value is not None:
if not isinstance(value, (int, float)) or value < min_val or value > max_val:
errors.append(f"Параметр {param} должен быть числом от {min_val} до {max_val}")
# Проверяем данные цепочек
chain_data = config_data.get('chain_data', [])
if chain_data:
for i, chain_item in enumerate(chain_data):
if not chain_item.get('ChainId'):
errors.append(f"Цепочка {i+1}: отсутствует ChainId")
if not chain_item.get('MergeItemId'):
errors.append(f"Цепочка {i+1}: отсутствует MergeItemId")
if not isinstance(chain_item.get('RequirementWeight', 0), (int, float)):
errors.append(f"Цепочка {i+1}: RequirementWeight должен быть числом")
if not isinstance(chain_item.get('RewardDifficulty', 0), (int, float)):
errors.append(f"Цепочка {i+1}: RewardDifficulty должен быть числом")
else:
warnings.append("Нет данных о цепочках - симуляция может не работать корректно")
# Проверяем веса требований
req_weights = config_data.get('requirement_weights', "70,30")
try:
weights = [int(w.strip()) for w in req_weights.split(',')]
if len(weights) != 2:
errors.append("Веса требований должны содержать ровно 2 значения")
elif any(w < 0 for w in weights):
errors.append("Веса требований должны быть положительными числами")
except:
errors.append("Неверный формат весов требований (ожидается 'число,число')")
is_valid = len(errors) == 0
return {
"success": True,
"is_valid": is_valid,
"errors": errors,
"warnings": warnings,
"message": "Конфигурация валидна" if is_valid else f"Найдено {len(errors)} ошибок"
}
except Exception as e:
return {
"success": False,
"error": str(e),
"message": f"Ошибка при валидации: {str(e)}"
}
# ===============================================================
# MCP SERVER INSTANCE
# ===============================================================
# Глобальный экземпляр MCP сервера
mcp_server = MergeSimulatorMCPServer()
# Экспорт функций для использования в других модулях
def get_mcp_server() -> MergeSimulatorMCPServer:
"""Возвращает экземпляр MCP сервера"""
return mcp_server
# Список всех доступных MCP функций
MCP_FUNCTIONS = {
"mcp_save_simulator_config": mcp_server.mcp_save_simulator_config,
"mcp_load_simulator_config": mcp_server.mcp_load_simulator_config,
"mcp_list_simulator_configs": mcp_server.mcp_list_simulator_configs,
"mcp_delete_simulator_config": mcp_server.mcp_delete_simulator_config,
"mcp_run_simulation": mcp_server.mcp_run_simulation,
"mcp_get_simulation_results": mcp_server.mcp_get_simulation_results,
"mcp_analyze_simulation_results": mcp_server.mcp_analyze_simulation_results,
"mcp_create_chain_data_template": mcp_server.mcp_create_chain_data_template,
"mcp_create_rewards_data_template": mcp_server.mcp_create_rewards_data_template,
"mcp_validate_config_data": mcp_server.mcp_validate_config_data
}