Skip to main content
Glama
assignment.py13 kB
"""Assignment problem tools for MCP server.""" import logging from typing import Any from fastmcp import FastMCP from mcp_optimizer.utils.resource_monitor import with_resource_limits logger = logging.getLogger(__name__) # Define functions that can be imported directly @with_resource_limits(timeout_seconds=60.0, estimated_memory_mb=80.0) def solve_assignment_problem( workers: list[str], tasks: list[str], costs: list[list[float]], maximize: bool = False, max_tasks_per_worker: int | None = None, min_tasks_per_worker: int | None = None, objective: str | None = None, constraints: dict[str, Any] | None = None, ) -> dict[str, Any]: """Solve assignment problem using OR-Tools.""" try: # Validate input if not workers: return { "status": "error", "total_cost": None, "assignments": [], "execution_time": 0.0, "error_message": "No workers provided", } if not tasks: return { "status": "error", "total_cost": None, "assignments": [], "execution_time": 0.0, "error_message": "No tasks provided", } if len(costs) != len(workers): return { "status": "error", "total_cost": None, "assignments": [], "execution_time": 0.0, "error_message": f"Cost matrix dimensions: rows ({len(costs)}) must match workers count ({len(workers)})", } for i, row in enumerate(costs): if len(row) != len(tasks): return { "status": "error", "total_cost": None, "assignments": [], "execution_time": 0.0, "error_message": f"Cost matrix row {i} length ({len(row)}) must match tasks count ({len(tasks)})", } # Handle objective parameter (convert to maximize flag) if objective is not None: maximize = objective.lower() == "maximize" # Handle constraints parameter if constraints is not None: # Check for unsupported constraint types forbidden_assignments = constraints.get("forbidden_assignments", []) task_requirements = constraints.get("task_requirements", {}) if forbidden_assignments: return { "status": "error", "total_cost": None, "assignments": [], "execution_time": 0.0, "error_message": "Forbidden assignments constraints are not currently supported", } if task_requirements: return { "status": "error", "total_cost": None, "assignments": [], "execution_time": 0.0, "error_message": "Task requirements constraints are not currently supported", } # Extract worker limits if provided worker_limits = constraints.get("worker_limits", {}) if worker_limits: # Convert worker limits to max_tasks_per_worker if all workers have same limit limit_values = list(worker_limits.values()) if len(set(limit_values)) == 1 and all( worker in worker_limits for worker in workers ): max_tasks_per_worker = limit_values[0] # If all workers have limit 0 and there are tasks, problem is infeasible if max_tasks_per_worker == 0 and tasks: return { "status": "infeasible", "total_cost": None, "assignments": [], "execution_time": 0.0, "error_message": "No worker can be assigned any tasks due to constraints", } else: return { "status": "error", "total_cost": None, "assignments": [], "execution_time": 0.0, "error_message": "Individual worker limits with different values are not currently supported", } # Create solver and solve from mcp_optimizer.solvers import ORToolsSolver solver = ORToolsSolver() result = solver.solve_assignment_problem( workers=workers, tasks=tasks, costs=costs, maximize=maximize, max_tasks_per_worker=max_tasks_per_worker, min_tasks_per_worker=min_tasks_per_worker, ) # Add objective to result if specified if objective is not None: result["objective"] = objective logger.info(f"Assignment problem solved with status: {result.get('status')}") return result except Exception as e: logger.error(f"Error in solve_assignment_problem: {e}") return { "status": "error", "total_cost": None, "assignments": [], "execution_time": 0.0, "error_message": f"Failed to solve assignment problem: {str(e)}", } def solve_transportation_problem( suppliers: list[dict[str, Any]], consumers: list[dict[str, Any]], costs: list[list[float]], ) -> dict[str, Any]: """Solve transportation problem using OR-Tools.""" try: # Validate input if not suppliers: return { "status": "error", "total_cost": None, "flows": [], "execution_time": 0.0, "error_message": "No suppliers provided", } if not consumers: return { "status": "error", "total_cost": None, "flows": [], "execution_time": 0.0, "error_message": "No consumers provided", } # Validate supplier format for i, supplier in enumerate(suppliers): if not isinstance(supplier, dict): return { "status": "error", "total_cost": None, "flows": [], "execution_time": 0.0, "error_message": f"Supplier {i} must be a dictionary", } if "name" not in supplier or "supply" not in supplier: return { "status": "error", "total_cost": None, "flows": [], "execution_time": 0.0, "error_message": f"Supplier {i} must have 'name' and 'supply' fields", } # Validate consumer format # type: ignore[unreachable] for i, consumer in enumerate(consumers): if not isinstance(consumer, dict): return { "status": "error", "total_cost": None, "flows": [], "execution_time": 0.0, "error_message": f"Consumer {i} must be a dictionary", } if "name" not in consumer or "demand" not in consumer: return { "status": "error", "total_cost": None, "flows": [], "execution_time": 0.0, "error_message": f"Consumer {i} must have 'name' and 'demand' fields", } # Validate cost matrix dimensions # type: ignore[unreachable] if len(costs) != len(suppliers): return { "status": "error", "total_cost": None, "flows": [], "execution_time": 0.0, "error_message": f"Cost matrix dimensions: rows ({len(costs)}) must match suppliers count ({len(suppliers)})", } for i, row in enumerate(costs): if len(row) != len(consumers): return { "status": "error", "total_cost": None, "flows": [], "execution_time": 0.0, "error_message": f"Cost matrix row {i} length ({len(row)}) must match consumers count ({len(consumers)})", } # Check for negative supply/demand for supplier in suppliers: if supplier["supply"] < 0: return { "status": "error", "total_cost": None, "flows": [], "execution_time": 0.0, "error_message": "Supply must be non-negative", } for consumer in consumers: if consumer["demand"] < 0: return { "status": "error", "total_cost": None, "flows": [], "execution_time": 0.0, "error_message": "Demand must be non-negative", } # Check supply-demand balance total_supply = sum(supplier["supply"] for supplier in suppliers) total_demand = sum(consumer["demand"] for consumer in consumers) if abs(total_supply - total_demand) > 1e-6: return { "status": "error", "total_cost": None, "flows": [], "execution_time": 0.0, "error_message": f"Total supply ({total_supply}) must equal total demand ({total_demand})", } # Create solver and solve from mcp_optimizer.solvers import ORToolsSolver solver = ORToolsSolver() result = solver.solve_transportation_problem( suppliers=suppliers, consumers=consumers, costs=costs, ) # Add shipments as alias for flows for backward compatibility if "flows" in result: result["shipments"] = result["flows"] logger.info(f"Transportation problem solved with status: {result.get('status')}") return result except Exception as e: logger.error(f"Error in solve_transportation_problem: {e}") return { "status": "error", "total_cost": None, "flows": [], "execution_time": 0.0, "error_message": f"Failed to solve transportation problem: {str(e)}", } def register_assignment_tools(mcp: FastMCP[Any]) -> None: """Register assignment and transportation problem tools.""" @mcp.tool() def solve_assignment_problem_tool( workers: list[str], tasks: list[str], costs: list[list[float]], maximize: bool = False, max_tasks_per_worker: int | None = None, min_tasks_per_worker: int | None = None, ) -> dict[str, Any]: """ Solve assignment problem using OR-Tools Hungarian algorithm. Args: workers: List of worker names tasks: List of task names costs: 2D cost matrix where costs[i][j] is cost of assigning worker i to task j maximize: Whether to maximize instead of minimize (default: False) max_tasks_per_worker: Maximum tasks per worker (optional) min_tasks_per_worker: Minimum tasks per worker (optional) Returns: Dictionary with solution status, assignments, total cost, and execution time """ result = solve_assignment_problem( workers=workers, tasks=tasks, costs=costs, maximize=maximize, max_tasks_per_worker=max_tasks_per_worker, min_tasks_per_worker=min_tasks_per_worker, ) result_dict: dict[str, Any] = result return result_dict @mcp.tool() def solve_transportation_problem_tool( suppliers: list[dict[str, Any]], consumers: list[dict[str, Any]], costs: list[list[float]], ) -> dict[str, Any]: """ Solve transportation problem using OR-Tools. Args: suppliers: List of supplier dictionaries with 'name' and 'supply' keys consumers: List of consumer dictionaries with 'name' and 'demand' keys costs: 2D cost matrix where costs[i][j] is cost of shipping from supplier i to consumer j Returns: Dictionary with solution status, flows, total cost, and execution time """ return solve_transportation_problem( suppliers=suppliers, consumers=consumers, costs=costs, ) logger.info("Registered assignment and transportation tools")

Implementation Reference

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/dmitryanchikov/mcp-optimizer'

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