nexus-5/core/mcp/tools/sessions.py
2026-01-26 11:09:40 -05:00

374 lines
12 KiB
Python

"""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]})