fix(backend): Fix MCP module compilation errors by using correct exception classes
Replaced non-existent ColaFlow.Shared.Kernel.Exceptions namespace references with ColaFlow.Modules.Mcp.Domain.Exceptions in 5 files: Changes: - McpToolRegistry.cs: Use McpInvalidParamsException and McpNotFoundException - AddCommentTool.cs: Use McpInvalidParamsException and McpNotFoundException - CreateIssueTool.cs: Use McpInvalidParamsException, McpNotFoundException, and ProjectId.From() - UpdateStatusTool.cs: Use McpNotFoundException - ToolParameterParser.cs: Use McpInvalidParamsException for all validation errors All BadRequestException -> McpInvalidParamsException All NotFoundException -> McpNotFoundException Also fixed CreateIssueTool to convert Guid to ProjectId value object. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
using ColaFlow.Modules.Mcp.Contracts.Tools;
|
||||||
|
|
||||||
|
namespace ColaFlow.Modules.Mcp.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registry interface for MCP Tools
|
||||||
|
/// Manages tool discovery and dispatching
|
||||||
|
/// </summary>
|
||||||
|
public interface IMcpToolRegistry
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get all registered tools
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<IMcpTool> GetAllTools();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get tool by name
|
||||||
|
/// </summary>
|
||||||
|
IMcpTool? GetTool(string toolName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if tool exists
|
||||||
|
/// </summary>
|
||||||
|
bool HasTool(string toolName);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute a tool by name
|
||||||
|
/// </summary>
|
||||||
|
Task<McpToolResult> ExecuteToolAsync(
|
||||||
|
McpToolCall toolCall,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
using ColaFlow.Modules.Mcp.Contracts.Tools;
|
||||||
|
using ColaFlow.Modules.Mcp.Domain.Exceptions;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace ColaFlow.Modules.Mcp.Application.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registry for MCP Tools with auto-discovery
|
||||||
|
/// Uses constructor injection to register all IMcpTool implementations
|
||||||
|
/// </summary>
|
||||||
|
public class McpToolRegistry : IMcpToolRegistry
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, IMcpTool> _tools;
|
||||||
|
private readonly ILogger<McpToolRegistry> _logger;
|
||||||
|
|
||||||
|
public McpToolRegistry(
|
||||||
|
IEnumerable<IMcpTool> tools,
|
||||||
|
ILogger<McpToolRegistry> _logger)
|
||||||
|
{
|
||||||
|
this._logger = _logger ?? throw new ArgumentNullException(nameof(_logger));
|
||||||
|
|
||||||
|
// Auto-discover and register all tools
|
||||||
|
_tools = new Dictionary<string, IMcpTool>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var tool in tools)
|
||||||
|
{
|
||||||
|
if (_tools.ContainsKey(tool.Name))
|
||||||
|
{
|
||||||
|
this._logger.LogWarning(
|
||||||
|
"Duplicate tool name detected: {ToolName}. Skipping duplicate registration.",
|
||||||
|
tool.Name);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_tools[tool.Name] = tool;
|
||||||
|
this._logger.LogInformation(
|
||||||
|
"Registered MCP Tool: {ToolName} - {Description}",
|
||||||
|
tool.Name, tool.Description);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._logger.LogInformation(
|
||||||
|
"McpToolRegistry initialized with {Count} tools",
|
||||||
|
_tools.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<IMcpTool> GetAllTools()
|
||||||
|
{
|
||||||
|
return _tools.Values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IMcpTool? GetTool(string toolName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(toolName))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
_tools.TryGetValue(toolName, out var tool);
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasTool(string toolName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(toolName))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return _tools.ContainsKey(toolName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McpToolResult> ExecuteToolAsync(
|
||||||
|
McpToolCall toolCall,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (toolCall == null)
|
||||||
|
throw new ArgumentNullException(nameof(toolCall));
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(toolCall.Name))
|
||||||
|
throw new McpInvalidParamsException("Tool name cannot be empty");
|
||||||
|
|
||||||
|
// Get tool
|
||||||
|
var tool = GetTool(toolCall.Name);
|
||||||
|
if (tool == null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(
|
||||||
|
"Tool not found: {ToolName}. Available tools: {AvailableTools}",
|
||||||
|
toolCall.Name, string.Join(", ", _tools.Keys));
|
||||||
|
|
||||||
|
throw new McpNotFoundException("Tool", toolCall.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"Executing MCP Tool: {ToolName}",
|
||||||
|
toolCall.Name);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Execute tool
|
||||||
|
var result = await tool.ExecuteAsync(toolCall, cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"MCP Tool executed successfully: {ToolName}, IsError={IsError}",
|
||||||
|
toolCall.Name, result.IsError);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex,
|
||||||
|
"Error executing MCP Tool: {ToolName}",
|
||||||
|
toolCall.Name);
|
||||||
|
|
||||||
|
// Return error result
|
||||||
|
return new McpToolResult
|
||||||
|
{
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new McpToolContent
|
||||||
|
{
|
||||||
|
Type = "text",
|
||||||
|
Text = $"Error executing tool '{toolCall.Name}': {ex.Message}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IsError = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
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.Repositories;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace ColaFlow.Modules.Mcp.Application.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP Tool: add_comment
|
||||||
|
/// Adds a comment to an existing Issue
|
||||||
|
/// Generates a Diff Preview and creates a PendingChange for approval
|
||||||
|
/// </summary>
|
||||||
|
public class AddCommentTool : IMcpTool
|
||||||
|
{
|
||||||
|
private readonly IPendingChangeService _pendingChangeService;
|
||||||
|
private readonly IIssueRepository _issueRepository;
|
||||||
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
|
private readonly DiffPreviewService _diffPreviewService;
|
||||||
|
private readonly ILogger<AddCommentTool> _logger;
|
||||||
|
|
||||||
|
public string Name => "add_comment";
|
||||||
|
|
||||||
|
public string Description => "Add a comment to an existing issue. " +
|
||||||
|
"Supports markdown formatting. " +
|
||||||
|
"Requires human approval before being added.";
|
||||||
|
|
||||||
|
public McpToolInputSchema InputSchema => new()
|
||||||
|
{
|
||||||
|
Type = "object",
|
||||||
|
Properties = new Dictionary<string, JsonSchemaProperty>
|
||||||
|
{
|
||||||
|
["issueId"] = new()
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Format = "uuid",
|
||||||
|
Description = "The ID of the issue to comment on"
|
||||||
|
},
|
||||||
|
["content"] = new()
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
MinLength = 1,
|
||||||
|
MaxLength = 2000,
|
||||||
|
Description = "The comment content (supports markdown, max 2000 characters)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Required = new List<string> { "issueId", "content" }
|
||||||
|
};
|
||||||
|
|
||||||
|
public AddCommentTool(
|
||||||
|
IPendingChangeService pendingChangeService,
|
||||||
|
IIssueRepository issueRepository,
|
||||||
|
IHttpContextAccessor httpContextAccessor,
|
||||||
|
DiffPreviewService diffPreviewService,
|
||||||
|
ILogger<AddCommentTool> logger)
|
||||||
|
{
|
||||||
|
_pendingChangeService = pendingChangeService ?? throw new ArgumentNullException(nameof(pendingChangeService));
|
||||||
|
_issueRepository = issueRepository ?? throw new ArgumentNullException(nameof(issueRepository));
|
||||||
|
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
|
||||||
|
_diffPreviewService = diffPreviewService ?? throw new ArgumentNullException(nameof(diffPreviewService));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McpToolResult> ExecuteAsync(
|
||||||
|
McpToolCall toolCall,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Executing add_comment tool");
|
||||||
|
|
||||||
|
// 1. Parse and validate input
|
||||||
|
var issueId = ToolParameterParser.ParseGuid(toolCall.Arguments, "issueId", required: true)!.Value;
|
||||||
|
var content = ToolParameterParser.ParseString(toolCall.Arguments, "content", required: true);
|
||||||
|
|
||||||
|
// Validate content
|
||||||
|
if (string.IsNullOrWhiteSpace(content))
|
||||||
|
throw new McpInvalidParamsException("Comment content cannot be empty");
|
||||||
|
|
||||||
|
if (content.Length > 2000)
|
||||||
|
throw new McpInvalidParamsException("Comment content cannot exceed 2000 characters");
|
||||||
|
|
||||||
|
// 2. Verify issue exists
|
||||||
|
var issue = await _issueRepository.GetByIdAsync(issueId, cancellationToken);
|
||||||
|
if (issue == null)
|
||||||
|
throw new McpNotFoundException("Issue", issueId.ToString());
|
||||||
|
|
||||||
|
// 3. Get API Key ID (to track who created the comment)
|
||||||
|
var apiKeyId = _httpContextAccessor.HttpContext?.Items["ApiKeyId"] as Guid?;
|
||||||
|
|
||||||
|
// 4. Build comment data for diff preview
|
||||||
|
var commentData = new
|
||||||
|
{
|
||||||
|
issueId = issueId,
|
||||||
|
content = content,
|
||||||
|
authorType = "AI",
|
||||||
|
authorId = apiKeyId,
|
||||||
|
createdAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
// 5. Generate Diff Preview (CREATE Comment operation)
|
||||||
|
var diff = _diffPreviewService.GenerateCreateDiff(
|
||||||
|
entityType: "Comment",
|
||||||
|
afterEntity: commentData,
|
||||||
|
entityKey: $"Comment on {issue.Type}-{issue.Id.ToString().Substring(0, 8)}"
|
||||||
|
);
|
||||||
|
|
||||||
|
// 6. Create PendingChange
|
||||||
|
var pendingChange = await _pendingChangeService.CreateAsync(
|
||||||
|
new CreatePendingChangeRequest
|
||||||
|
{
|
||||||
|
ToolName = Name,
|
||||||
|
Diff = diff,
|
||||||
|
ExpirationHours = 24
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"PendingChange created: {PendingChangeId} - CREATE Comment on Issue {IssueId}",
|
||||||
|
pendingChange.Id, issueId);
|
||||||
|
|
||||||
|
// 7. Return pendingChangeId to AI
|
||||||
|
return new McpToolResult
|
||||||
|
{
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new McpToolContent
|
||||||
|
{
|
||||||
|
Type = "text",
|
||||||
|
Text = $"Comment creation request submitted for approval.\n\n" +
|
||||||
|
$"**Pending Change ID**: {pendingChange.Id}\n" +
|
||||||
|
$"**Status**: Pending Approval\n" +
|
||||||
|
$"**Issue**: {issue.Title}\n" +
|
||||||
|
$"**Comment Preview**: {(content.Length > 100 ? content.Substring(0, 100) + "..." : content)}\n\n" +
|
||||||
|
$"A human user must approve this change before the comment is added. " +
|
||||||
|
$"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 add_comment tool");
|
||||||
|
|
||||||
|
return new McpToolResult
|
||||||
|
{
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new McpToolContent
|
||||||
|
{
|
||||||
|
Type = "text",
|
||||||
|
Text = $"Error: {ex.Message}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IsError = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
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 : IMcpTool
|
||||||
|
{
|
||||||
|
private readonly IPendingChangeService _pendingChangeService;
|
||||||
|
private readonly IProjectRepository _projectRepository;
|
||||||
|
private readonly ITenantContext _tenantContext;
|
||||||
|
private readonly DiffPreviewService _diffPreviewService;
|
||||||
|
private readonly ILogger<CreateIssueTool> _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 CreateIssueTool(
|
||||||
|
IPendingChangeService pendingChangeService,
|
||||||
|
IProjectRepository projectRepository,
|
||||||
|
ITenantContext tenantContext,
|
||||||
|
DiffPreviewService diffPreviewService,
|
||||||
|
ILogger<CreateIssueTool> logger)
|
||||||
|
{
|
||||||
|
_pendingChangeService = pendingChangeService ?? throw new ArgumentNullException(nameof(pendingChangeService));
|
||||||
|
_projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||||
|
_tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
|
||||||
|
_diffPreviewService = diffPreviewService ?? throw new ArgumentNullException(nameof(diffPreviewService));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
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.IssueManagement.Domain.Repositories;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
|
namespace ColaFlow.Modules.Mcp.Application.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MCP Tool: update_status
|
||||||
|
/// Updates the status of an existing Issue
|
||||||
|
/// Generates a Diff Preview and creates a PendingChange for approval
|
||||||
|
/// </summary>
|
||||||
|
public class UpdateStatusTool : IMcpTool
|
||||||
|
{
|
||||||
|
private readonly IPendingChangeService _pendingChangeService;
|
||||||
|
private readonly IIssueRepository _issueRepository;
|
||||||
|
private readonly DiffPreviewService _diffPreviewService;
|
||||||
|
private readonly ILogger<UpdateStatusTool> _logger;
|
||||||
|
|
||||||
|
public string Name => "update_status";
|
||||||
|
|
||||||
|
public string Description => "Update the status of an existing issue. " +
|
||||||
|
"Supports workflow transitions (Backlog → Todo → InProgress → Done). " +
|
||||||
|
"Requires human approval before being applied.";
|
||||||
|
|
||||||
|
public McpToolInputSchema InputSchema => new()
|
||||||
|
{
|
||||||
|
Type = "object",
|
||||||
|
Properties = new Dictionary<string, JsonSchemaProperty>
|
||||||
|
{
|
||||||
|
["issueId"] = new()
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Format = "uuid",
|
||||||
|
Description = "The ID of the issue to update"
|
||||||
|
},
|
||||||
|
["newStatus"] = new()
|
||||||
|
{
|
||||||
|
Type = "string",
|
||||||
|
Enum = new[] { "Backlog", "Todo", "InProgress", "Done" },
|
||||||
|
Description = "The new status to set"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Required = new List<string> { "issueId", "newStatus" }
|
||||||
|
};
|
||||||
|
|
||||||
|
public UpdateStatusTool(
|
||||||
|
IPendingChangeService pendingChangeService,
|
||||||
|
IIssueRepository issueRepository,
|
||||||
|
DiffPreviewService diffPreviewService,
|
||||||
|
ILogger<UpdateStatusTool> logger)
|
||||||
|
{
|
||||||
|
_pendingChangeService = pendingChangeService ?? throw new ArgumentNullException(nameof(pendingChangeService));
|
||||||
|
_issueRepository = issueRepository ?? throw new ArgumentNullException(nameof(issueRepository));
|
||||||
|
_diffPreviewService = diffPreviewService ?? throw new ArgumentNullException(nameof(diffPreviewService));
|
||||||
|
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<McpToolResult> ExecuteAsync(
|
||||||
|
McpToolCall toolCall,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Executing update_status tool");
|
||||||
|
|
||||||
|
// 1. Parse and validate input
|
||||||
|
var issueId = ToolParameterParser.ParseGuid(toolCall.Arguments, "issueId", required: true)!.Value;
|
||||||
|
var newStatus = ToolParameterParser.ParseEnum<IssueStatus>(toolCall.Arguments, "newStatus", required: true)!.Value;
|
||||||
|
|
||||||
|
// 2. Fetch current issue
|
||||||
|
var issue = await _issueRepository.GetByIdAsync(issueId, cancellationToken);
|
||||||
|
if (issue == null)
|
||||||
|
throw new McpNotFoundException("Issue", issueId.ToString());
|
||||||
|
|
||||||
|
var oldStatus = issue.Status;
|
||||||
|
|
||||||
|
// 3. Build before and after data for diff preview
|
||||||
|
var beforeData = new
|
||||||
|
{
|
||||||
|
id = issue.Id,
|
||||||
|
title = issue.Title,
|
||||||
|
type = issue.Type.ToString(),
|
||||||
|
status = oldStatus.ToString(),
|
||||||
|
priority = issue.Priority.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
var afterData = new
|
||||||
|
{
|
||||||
|
id = issue.Id,
|
||||||
|
title = issue.Title,
|
||||||
|
type = issue.Type.ToString(),
|
||||||
|
status = newStatus.ToString(), // Only status changed
|
||||||
|
priority = issue.Priority.ToString()
|
||||||
|
};
|
||||||
|
|
||||||
|
// 4. Generate Diff Preview (UPDATE operation)
|
||||||
|
var diff = _diffPreviewService.GenerateUpdateDiff(
|
||||||
|
entityType: "Issue",
|
||||||
|
entityId: issueId,
|
||||||
|
beforeEntity: beforeData,
|
||||||
|
afterEntity: afterData,
|
||||||
|
entityKey: $"{issue.Type}-{issue.Id.ToString().Substring(0, 8)}" // Simplified key
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. Create PendingChange
|
||||||
|
var pendingChange = await _pendingChangeService.CreateAsync(
|
||||||
|
new CreatePendingChangeRequest
|
||||||
|
{
|
||||||
|
ToolName = Name,
|
||||||
|
Diff = diff,
|
||||||
|
ExpirationHours = 24
|
||||||
|
},
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"PendingChange created: {PendingChangeId} - UPDATE Issue {IssueId} status: {OldStatus} → {NewStatus}",
|
||||||
|
pendingChange.Id, issueId, oldStatus, newStatus);
|
||||||
|
|
||||||
|
// 6. Return pendingChangeId to AI
|
||||||
|
return new McpToolResult
|
||||||
|
{
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new McpToolContent
|
||||||
|
{
|
||||||
|
Type = "text",
|
||||||
|
Text = $"Issue status update request submitted for approval.\n\n" +
|
||||||
|
$"**Pending Change ID**: {pendingChange.Id}\n" +
|
||||||
|
$"**Status**: Pending Approval\n" +
|
||||||
|
$"**Issue**: {issue.Title}\n" +
|
||||||
|
$"**Old Status**: {oldStatus}\n" +
|
||||||
|
$"**New Status**: {newStatus}\n\n" +
|
||||||
|
$"A human user must approve this change before the issue status is updated. " +
|
||||||
|
$"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 update_status tool");
|
||||||
|
|
||||||
|
return new McpToolResult
|
||||||
|
{
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new McpToolContent
|
||||||
|
{
|
||||||
|
Type = "text",
|
||||||
|
Text = $"Error: {ex.Message}"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
IsError = true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using ColaFlow.Modules.Mcp.Domain.Exceptions;
|
||||||
|
|
||||||
|
namespace ColaFlow.Modules.Mcp.Application.Tools.Validation;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper class for parsing and validating tool parameters
|
||||||
|
/// </summary>
|
||||||
|
public static class ToolParameterParser
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a required string parameter
|
||||||
|
/// </summary>
|
||||||
|
public static string ParseString(Dictionary<string, object> args, string paramName, bool required = false)
|
||||||
|
{
|
||||||
|
if (!args.TryGetValue(paramName, out var value))
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' is missing");
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be null");
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JsonElement (from JSON deserialization)
|
||||||
|
if (value is JsonElement jsonElement)
|
||||||
|
{
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.String)
|
||||||
|
return jsonElement.GetString() ?? string.Empty;
|
||||||
|
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.Null && required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be null");
|
||||||
|
|
||||||
|
return jsonElement.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a required Guid parameter
|
||||||
|
/// </summary>
|
||||||
|
public static Guid? ParseGuid(Dictionary<string, object> args, string paramName, bool required = false)
|
||||||
|
{
|
||||||
|
if (!args.TryGetValue(paramName, out var value))
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' is missing");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JsonElement
|
||||||
|
if (value is JsonElement jsonElement)
|
||||||
|
{
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var strValue = jsonElement.GetString();
|
||||||
|
if (string.IsNullOrWhiteSpace(strValue))
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be empty");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Guid.TryParse(strValue, out var guid))
|
||||||
|
return guid;
|
||||||
|
|
||||||
|
throw new McpInvalidParamsException($"Parameter '{paramName}' must be a valid UUID");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.Null && required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle string
|
||||||
|
var stringValue = value.ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(stringValue))
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be empty");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Guid.TryParse(stringValue, out var result))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
throw new McpInvalidParamsException($"Parameter '{paramName}' must be a valid UUID");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse an enum parameter
|
||||||
|
/// </summary>
|
||||||
|
public static TEnum? ParseEnum<TEnum>(Dictionary<string, object> args, string paramName, bool required = false)
|
||||||
|
where TEnum : struct, Enum
|
||||||
|
{
|
||||||
|
if (!args.TryGetValue(paramName, out var value))
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' is missing");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JsonElement
|
||||||
|
if (value is JsonElement jsonElement)
|
||||||
|
{
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var strValue = jsonElement.GetString();
|
||||||
|
if (string.IsNullOrWhiteSpace(strValue))
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be empty");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Enum.TryParse<TEnum>(strValue, ignoreCase: true, out var enumValue))
|
||||||
|
return enumValue;
|
||||||
|
|
||||||
|
var validValues = string.Join(", ", Enum.GetNames<TEnum>());
|
||||||
|
throw new McpInvalidParamsException(
|
||||||
|
$"Parameter '{paramName}' must be one of: {validValues}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.Null && required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle string
|
||||||
|
var stringValue = value.ToString();
|
||||||
|
if (string.IsNullOrWhiteSpace(stringValue))
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be empty");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Enum.TryParse<TEnum>(stringValue, ignoreCase: true, out var result))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
var validValuesList = string.Join(", ", Enum.GetNames<TEnum>());
|
||||||
|
throw new McpInvalidParamsException(
|
||||||
|
$"Parameter '{paramName}' must be one of: {validValuesList}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a decimal parameter
|
||||||
|
/// </summary>
|
||||||
|
public static decimal? ParseDecimal(Dictionary<string, object> args, string paramName, bool required = false)
|
||||||
|
{
|
||||||
|
if (!args.TryGetValue(paramName, out var value))
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' is missing");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JsonElement
|
||||||
|
if (value is JsonElement jsonElement)
|
||||||
|
{
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.Number)
|
||||||
|
return jsonElement.GetDecimal();
|
||||||
|
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var strValue = jsonElement.GetString();
|
||||||
|
if (string.IsNullOrWhiteSpace(strValue))
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be empty");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decimal.TryParse(strValue, out var decimalValue))
|
||||||
|
return decimalValue;
|
||||||
|
|
||||||
|
throw new McpInvalidParamsException($"Parameter '{paramName}' must be a valid number");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.Null && required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to convert directly
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Convert.ToDecimal(value);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
throw new McpInvalidParamsException($"Parameter '{paramName}' must be a valid number");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse an integer parameter
|
||||||
|
/// </summary>
|
||||||
|
public static int? ParseInt(Dictionary<string, object> args, string paramName, bool required = false)
|
||||||
|
{
|
||||||
|
if (!args.TryGetValue(paramName, out var value))
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' is missing");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JsonElement
|
||||||
|
if (value is JsonElement jsonElement)
|
||||||
|
{
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.Number)
|
||||||
|
return jsonElement.GetInt32();
|
||||||
|
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var strValue = jsonElement.GetString();
|
||||||
|
if (string.IsNullOrWhiteSpace(strValue))
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be empty");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (int.TryParse(strValue, out var intValue))
|
||||||
|
return intValue;
|
||||||
|
|
||||||
|
throw new McpInvalidParamsException($"Parameter '{paramName}' must be a valid integer");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.Null && required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to convert directly
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Convert.ToInt32(value);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
throw new McpInvalidParamsException($"Parameter '{paramName}' must be a valid integer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Parse a boolean parameter
|
||||||
|
/// </summary>
|
||||||
|
public static bool? ParseBool(Dictionary<string, object> args, string paramName, bool required = false)
|
||||||
|
{
|
||||||
|
if (!args.TryGetValue(paramName, out var value))
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' is missing");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JsonElement
|
||||||
|
if (value is JsonElement jsonElement)
|
||||||
|
{
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.True)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.False)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var strValue = jsonElement.GetString();
|
||||||
|
if (string.IsNullOrWhiteSpace(strValue))
|
||||||
|
{
|
||||||
|
if (required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be empty");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bool.TryParse(strValue, out var boolValue))
|
||||||
|
return boolValue;
|
||||||
|
|
||||||
|
throw new McpInvalidParamsException($"Parameter '{paramName}' must be a valid boolean");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.Null && required)
|
||||||
|
throw new McpInvalidParamsException($"Required parameter '{paramName}' cannot be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to convert directly
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Convert.ToBoolean(value);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
throw new McpInvalidParamsException($"Parameter '{paramName}' must be a valid boolean");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
namespace ColaFlow.Modules.Mcp.Contracts.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for MCP Tools
|
||||||
|
/// Tools provide write operations to AI agents through the MCP protocol
|
||||||
|
/// All write operations create PendingChanges and require human approval
|
||||||
|
/// </summary>
|
||||||
|
public interface IMcpTool
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tool name (e.g., "create_issue", "update_status")
|
||||||
|
/// Must be unique and follow snake_case naming convention
|
||||||
|
/// </summary>
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Human-readable tool description for AI to understand when to use this tool
|
||||||
|
/// </summary>
|
||||||
|
string Description { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSON Schema describing the tool's input parameters
|
||||||
|
/// </summary>
|
||||||
|
McpToolInputSchema InputSchema { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Execute the tool with the provided arguments
|
||||||
|
/// This should create a PendingChange, NOT execute the change directly
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="toolCall">The tool call request with arguments</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>Tool execution result</returns>
|
||||||
|
Task<McpToolResult> ExecuteAsync(
|
||||||
|
McpToolCall toolCall,
|
||||||
|
CancellationToken cancellationToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get tool descriptor with full metadata
|
||||||
|
/// </summary>
|
||||||
|
McpToolDescriptor GetDescriptor()
|
||||||
|
{
|
||||||
|
return new McpToolDescriptor
|
||||||
|
{
|
||||||
|
Name = Name,
|
||||||
|
Description = Description,
|
||||||
|
InputSchema = InputSchema
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
namespace ColaFlow.Modules.Mcp.Contracts.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a tool call request from an AI agent
|
||||||
|
/// </summary>
|
||||||
|
public sealed class McpToolCall
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tool name to execute
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tool arguments as key-value pairs
|
||||||
|
/// Values can be strings, numbers, booleans, arrays, or objects
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, object> Arguments { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace ColaFlow.Modules.Mcp.Contracts.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Descriptor for an MCP Tool containing metadata and schema
|
||||||
|
/// </summary>
|
||||||
|
public sealed class McpToolDescriptor
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tool name
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tool description
|
||||||
|
/// </summary>
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Input parameter schema
|
||||||
|
/// </summary>
|
||||||
|
public McpToolInputSchema InputSchema { get; set; } = new();
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
namespace ColaFlow.Modules.Mcp.Contracts.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSON Schema for tool input parameters
|
||||||
|
/// </summary>
|
||||||
|
public sealed class McpToolInputSchema
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Schema type (always "object" for tool inputs)
|
||||||
|
/// </summary>
|
||||||
|
public string Type { get; set; } = "object";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Schema properties (parameter definitions)
|
||||||
|
/// Key is parameter name, value is parameter schema
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, JsonSchemaProperty> Properties { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of required parameter names
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Required { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSON Schema property definition
|
||||||
|
/// </summary>
|
||||||
|
public sealed class JsonSchemaProperty
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Property type: "string", "number", "integer", "boolean", "array", "object"
|
||||||
|
/// </summary>
|
||||||
|
public string Type { get; set; } = "string";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Property description (for AI to understand)
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enum values (for restricted choices)
|
||||||
|
/// </summary>
|
||||||
|
public string[]? Enum { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// String format hint: "uuid", "email", "date-time", "uri", etc.
|
||||||
|
/// </summary>
|
||||||
|
public string? Format { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum value (for numbers)
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Minimum { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum value (for numbers)
|
||||||
|
/// </summary>
|
||||||
|
public decimal? Maximum { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Minimum length (for strings)
|
||||||
|
/// </summary>
|
||||||
|
public int? MinLength { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum length (for strings)
|
||||||
|
/// </summary>
|
||||||
|
public int? MaxLength { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pattern (regex) for string validation
|
||||||
|
/// </summary>
|
||||||
|
public string? Pattern { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Items schema (for arrays)
|
||||||
|
/// </summary>
|
||||||
|
public JsonSchemaProperty? Items { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Properties schema (for nested objects)
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, JsonSchemaProperty>? Properties { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Default value
|
||||||
|
/// </summary>
|
||||||
|
public object? Default { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
namespace ColaFlow.Modules.Mcp.Contracts.Tools;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of a tool execution
|
||||||
|
/// </summary>
|
||||||
|
public sealed class McpToolResult
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Tool result content (typically text describing the PendingChange created)
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<McpToolContent> Content { get; set; } = Array.Empty<McpToolContent>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the tool execution failed
|
||||||
|
/// </summary>
|
||||||
|
public bool IsError { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Content item in a tool result
|
||||||
|
/// </summary>
|
||||||
|
public sealed class McpToolContent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Content type: "text" or "resource"
|
||||||
|
/// </summary>
|
||||||
|
public string Type { get; set; } = "text";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Text content (for type="text")
|
||||||
|
/// </summary>
|
||||||
|
public string? Text { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resource URI (for type="resource")
|
||||||
|
/// </summary>
|
||||||
|
public string? Resource { get; set; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user