Files
ColaFlow/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/CreateIssueTool.cs
Yaojia Wang 63ff1a9914
Some checks failed
Code Coverage / Generate Coverage Report (push) Has been cancelled
Tests / Run Tests (9.0.x) (push) Has been cancelled
Tests / Docker Build Test (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Clean up
2025-11-09 18:40:36 +01:00

191 lines
7.9 KiB
C#

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;
/// <summary>
/// MCP Tool: create_issue
/// Creates a new Issue (Epic, Story, Task, or Bug)
/// Generates a Diff Preview and creates a PendingChange for approval
/// </summary>
public class CreateIssueTool(
IPendingChangeService pendingChangeService,
IProjectRepository projectRepository,
ITenantContext tenantContext,
DiffPreviewService diffPreviewService,
ILogger<CreateIssueTool> 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<CreateIssueTool> _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<string, JsonSchemaProperty>
{
["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<string> { "projectId", "title", "type" }
};
public async Task<McpToolResult> 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<IssueType>(toolCall.Arguments, "type", required: true)!.Value;
var priority = ToolParameterParser.ParseEnum<IssuePriority>(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
};
}
}
}