using System.Text.Json; using ColaFlow.Modules.Mcp.Application.DTOs; using ColaFlow.Modules.Mcp.Application.Services; using ColaFlow.Modules.Mcp.Application.Tools.Validation; using ColaFlow.Modules.Mcp.Contracts.Tools; using ColaFlow.Modules.Mcp.Domain.Exceptions; using ColaFlow.Modules.Mcp.Domain.Services; using ColaFlow.Modules.IssueManagement.Domain.Enums; using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces; using ColaFlow.Modules.ProjectManagement.Domain.Repositories; using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects; using Microsoft.Extensions.Logging; namespace ColaFlow.Modules.Mcp.Application.Tools; /// /// MCP Tool: create_issue /// Creates a new Issue (Epic, Story, Task, or Bug) /// Generates a Diff Preview and creates a PendingChange for approval /// public class CreateIssueTool( IPendingChangeService pendingChangeService, IProjectRepository projectRepository, ITenantContext tenantContext, DiffPreviewService diffPreviewService, ILogger logger) : IMcpTool { private readonly IPendingChangeService _pendingChangeService = pendingChangeService ?? throw new ArgumentNullException(nameof(pendingChangeService)); private readonly IProjectRepository _projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository)); private readonly ITenantContext _tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext)); private readonly DiffPreviewService _diffPreviewService = diffPreviewService ?? throw new ArgumentNullException(nameof(diffPreviewService)); private readonly ILogger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); public string Name => "create_issue"; public string Description => "Create a new issue (Epic, Story, Task, or Bug) in a ColaFlow project. " + "The issue will be created in 'Backlog' status and requires human approval before being created."; public McpToolInputSchema InputSchema => new() { Type = "object", Properties = new Dictionary { ["projectId"] = new() { Type = "string", Format = "uuid", Description = "The ID of the project to create the issue in" }, ["title"] = new() { Type = "string", MinLength = 1, MaxLength = 200, Description = "Issue title (max 200 characters)" }, ["description"] = new() { Type = "string", MaxLength = 2000, Description = "Detailed issue description (optional, max 2000 characters)" }, ["type"] = new() { Type = "string", Enum = new[] { "Epic", "Story", "Task", "Bug" }, Description = "Issue type" }, ["priority"] = new() { Type = "string", Enum = new[] { "Low", "Medium", "High", "Critical" }, Description = "Issue priority (optional, defaults to Medium)" }, ["assigneeId"] = new() { Type = "string", Format = "uuid", Description = "User ID to assign the issue to (optional)" } }, Required = new List { "projectId", "title", "type" } }; public async Task ExecuteAsync( McpToolCall toolCall, CancellationToken cancellationToken) { try { _logger.LogInformation("Executing create_issue tool"); // 1. Parse and validate input var projectId = ToolParameterParser.ParseGuid(toolCall.Arguments, "projectId", required: true)!.Value; var title = ToolParameterParser.ParseString(toolCall.Arguments, "title", required: true); var description = ToolParameterParser.ParseString(toolCall.Arguments, "description") ?? string.Empty; var type = ToolParameterParser.ParseEnum(toolCall.Arguments, "type", required: true)!.Value; var priority = ToolParameterParser.ParseEnum(toolCall.Arguments, "priority") ?? IssuePriority.Medium; var assigneeId = ToolParameterParser.ParseGuid(toolCall.Arguments, "assigneeId"); // Validate title if (string.IsNullOrWhiteSpace(title)) throw new McpInvalidParamsException("Issue title cannot be empty"); if (title.Length > 200) throw new McpInvalidParamsException("Issue title cannot exceed 200 characters"); if (description.Length > 2000) throw new McpInvalidParamsException("Issue description cannot exceed 2000 characters"); // 2. Verify project exists var project = await _projectRepository.GetByIdAsync(ProjectId.From(projectId), cancellationToken); if (project == null) throw new McpNotFoundException("Project", projectId.ToString()); // 3. Build "after data" object for diff preview var afterData = new { projectId = projectId, title = title, description = description, type = type.ToString(), priority = priority.ToString(), status = IssueStatus.Backlog.ToString(), // Default status assigneeId = assigneeId }; // 4. Generate Diff Preview (CREATE operation) var diff = _diffPreviewService.GenerateCreateDiff( entityType: "Issue", afterEntity: afterData, entityKey: null // No key yet (will be generated on approval) ); // 5. Create PendingChange (do NOT execute yet) var pendingChange = await _pendingChangeService.CreateAsync( new CreatePendingChangeRequest { ToolName = Name, Diff = diff, ExpirationHours = 24 }, cancellationToken); _logger.LogInformation( "PendingChange created: {PendingChangeId} - CREATE Issue: {Title}", pendingChange.Id, title); // 6. Return pendingChangeId to AI (NOT the created issue) return new McpToolResult { Content = new[] { new McpToolContent { Type = "text", Text = $"Issue creation request submitted for approval.\n\n" + $"**Pending Change ID**: {pendingChange.Id}\n" + $"**Status**: Pending Approval\n" + $"**Issue Type**: {type}\n" + $"**Title**: {title}\n" + $"**Priority**: {priority}\n" + $"**Project**: {project.Name}\n\n" + $"A human user must approve this change before the issue is created. " + $"The change will expire at {pendingChange.ExpiresAt:yyyy-MM-dd HH:mm} UTC if not approved." } }, IsError = false }; } catch (Exception ex) { _logger.LogError(ex, "Error executing create_issue tool"); return new McpToolResult { Content = new[] { new McpToolContent { Type = "text", Text = $"Error: {ex.Message}" } }, IsError = true }; } } }