"""Session tools for MCP.""" from typing import Optional from core.mcp.auth import MCPContext, Role, check_entity_access, execute_graphql from core.mcp.base import mcp, json_response, error_response @mcp.tool() async def get_active_session(entity_type: str, entity_id: str) -> str: """ Get the active session for a service or project. Args: entity_type: Either 'service' or 'project' entity_id: UUID of the service or project Returns: JSON object with session details or null if no active session """ profile = MCPContext.get_profile() if not profile: return error_response("No active profile. Call set_active_profile first.") try: await check_entity_access(entity_type, entity_id) except PermissionError as e: return error_response(str(e)) if entity_type == "service": query = """ query GetActiveSession($serviceId: UUID!) { activeServiceSession(serviceId: $serviceId) { id start end createdBy { id firstName lastName } } } """ variables = {"serviceId": entity_id} result_key = "activeServiceSession" elif entity_type == "project": query = """ query GetActiveSession($projectId: UUID!) { activeProjectSession(projectId: $projectId) { id start end createdBy { id firstName lastName } } } """ variables = {"projectId": entity_id} result_key = "activeProjectSession" else: return error_response("entity_type must be 'service' or 'project'") result = await execute_graphql(query, variables) if "errors" in result: return json_response(result) session = result["data"].get(result_key) return json_response({"active": session is not None, "session": session}) @mcp.tool() async def open_session(entity_type: str, entity_id: str) -> str: """ Start a work session for a service or project. - ADMIN: Any service/project - TEAM_MEMBER: Only if assigned Args: entity_type: Either 'service' or 'project' entity_id: UUID of the service or project Returns: JSON object with opened session details """ profile = MCPContext.get_profile() if not profile: return error_response("No active profile. Call set_active_profile first.") if profile.role == Role.TEAM_LEADER.value: return error_response("Access denied. TEAM_LEADER role is view-only.") try: await check_entity_access(entity_type, entity_id) except PermissionError as e: return error_response(str(e)) if entity_type == "service": mutation = """ mutation OpenSession($input: OpenServiceSessionInput!) { openServiceSession(input: $input) { id start service { id status } } } """ variables = {"input": {"serviceId": entity_id}} result_key = "openServiceSession" elif entity_type == "project": mutation = """ mutation OpenSession($input: ProjectSessionStartInput!) { openProjectSession(input: $input) { id start project { id status } } } """ variables = {"input": {"projectId": entity_id}} result_key = "openProjectSession" else: return error_response("entity_type must be 'service' or 'project'") result = await execute_graphql(mutation, variables) if "errors" in result: return json_response(result) return json_response({"success": True, "session": result["data"][result_key]}) @mcp.tool() async def close_session( entity_type: str, entity_id: str, completed_task_ids: Optional[str] = None ) -> str: """ Complete a work session and mark tasks as done. - ADMIN: Any session - TEAM_MEMBER: Only their own sessions Args: entity_type: Either 'service' or 'project' entity_id: UUID of the service or project completed_task_ids: Comma-separated UUIDs of completed tasks Returns: JSON object with closed session details """ profile = MCPContext.get_profile() if not profile: return error_response("No active profile. Call set_active_profile first.") if profile.role == Role.TEAM_LEADER.value: return error_response("Access denied. TEAM_LEADER role is view-only.") try: await check_entity_access(entity_type, entity_id) except PermissionError as e: return error_response(str(e)) task_ids = [] if completed_task_ids: task_ids = [tid.strip() for tid in completed_task_ids.split(",")] if entity_type == "service": mutation = """ mutation CloseSession($input: CloseServiceSessionInput!) { closeServiceSession(input: $input) { id start end service { id status } } } """ variables = {"input": {"serviceId": entity_id, "taskIds": task_ids}} result_key = "closeServiceSession" elif entity_type == "project": mutation = """ mutation CloseSession($input: ProjectSessionCloseInput!) { closeProjectSession(input: $input) { id start end project { id status } } } """ variables = {"input": {"projectId": entity_id, "taskIds": task_ids}} result_key = "closeProjectSession" else: return error_response("entity_type must be 'service' or 'project'") result = await execute_graphql(mutation, variables) if "errors" in result: return json_response(result) return json_response({"success": True, "session": result["data"][result_key]}) @mcp.tool() async def revert_session(entity_type: str, entity_id: str) -> str: """ Cancel an active session and revert status to SCHEDULED. - ADMIN: Any session - TEAM_MEMBER: Only their own sessions Args: entity_type: Either 'service' or 'project' entity_id: UUID of the service or project Returns: JSON object confirming reversion """ profile = MCPContext.get_profile() if not profile: return error_response("No active profile. Call set_active_profile first.") if profile.role == Role.TEAM_LEADER.value: return error_response("Access denied. TEAM_LEADER role is view-only.") try: await check_entity_access(entity_type, entity_id) except PermissionError as e: return error_response(str(e)) if entity_type == "service": mutation = """ mutation RevertSession($input: RevertServiceSessionInput!) { revertServiceSession(input: $input) } """ variables = {"input": {"serviceId": entity_id}} result_key = "revertServiceSession" elif entity_type == "project": mutation = """ mutation RevertSession($input: ProjectSessionRevertInput!) { revertProjectSession(input: $input) } """ variables = {"input": {"projectId": entity_id}} result_key = "revertProjectSession" else: return error_response("entity_type must be 'service' or 'project'") result = await execute_graphql(mutation, variables) if "errors" in result: return json_response(result) return json_response({"success": True, "reverted": result["data"][result_key]}) @mcp.tool() async def add_task_completion( entity_type: str, entity_id: str, task_id: str, notes: Optional[str] = None ) -> str: """ Mark a task as completed during an active session. Args: entity_type: Either 'service' or 'project' entity_id: UUID of the service or project task_id: UUID of the task to mark complete notes: Optional notes about task completion Returns: JSON object confirming task completion """ profile = MCPContext.get_profile() if not profile: return error_response("No active profile. Call set_active_profile first.") if profile.role == Role.TEAM_LEADER.value: return error_response("Access denied. TEAM_LEADER role is view-only.") try: await check_entity_access(entity_type, entity_id) except PermissionError as e: return error_response(str(e)) if entity_type == "service": mutation = """ mutation AddTaskCompletion($serviceId: ID!, $taskId: ID!, $notes: String) { addTaskCompletion(serviceId: $serviceId, taskId: $taskId, notes: $notes) { id } } """ variables = {"serviceId": entity_id, "taskId": task_id, "notes": notes} result_key = "addTaskCompletion" elif entity_type == "project": mutation = """ mutation AddTaskCompletion($projectId: ID!, $taskId: ID!, $notes: String) { addProjectTaskCompletion(projectId: $projectId, taskId: $taskId, notes: $notes) { id } } """ variables = {"projectId": entity_id, "taskId": task_id, "notes": notes} result_key = "addProjectTaskCompletion" else: return error_response("entity_type must be 'service' or 'project'") result = await execute_graphql(mutation, variables) if "errors" in result: return json_response(result) return json_response({"success": True, "completion": result["data"][result_key]}) @mcp.tool() async def remove_task_completion( entity_type: str, entity_id: str, task_id: str ) -> str: """ Unmark a task completion from an active session. Args: entity_type: Either 'service' or 'project' entity_id: UUID of the service or project task_id: UUID of the task to unmark Returns: JSON object confirming removal """ profile = MCPContext.get_profile() if not profile: return error_response("No active profile. Call set_active_profile first.") if profile.role == Role.TEAM_LEADER.value: return error_response("Access denied. TEAM_LEADER role is view-only.") try: await check_entity_access(entity_type, entity_id) except PermissionError as e: return error_response(str(e)) if entity_type == "service": mutation = """ mutation RemoveTaskCompletion($serviceId: ID!, $taskId: ID!) { removeTaskCompletion(serviceId: $serviceId, taskId: $taskId) { id } } """ variables = {"serviceId": entity_id, "taskId": task_id} result_key = "removeTaskCompletion" elif entity_type == "project": mutation = """ mutation RemoveTaskCompletion($projectId: ID!, $taskId: ID!) { removeProjectTaskCompletion(projectId: $projectId, taskId: $taskId) { id } } """ variables = {"projectId": entity_id, "taskId": task_id} result_key = "removeProjectTaskCompletion" else: return error_response("entity_type must be 'service' or 'project'") result = await execute_graphql(mutation, variables) if "errors" in result: return json_response(result) return json_response({"success": True, "removed": result["data"][result_key]})