diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/IMcpToolRegistry.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/IMcpToolRegistry.cs
new file mode 100644
index 0000000..84ca248
--- /dev/null
+++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/IMcpToolRegistry.cs
@@ -0,0 +1,32 @@
+using ColaFlow.Modules.Mcp.Contracts.Tools;
+
+namespace ColaFlow.Modules.Mcp.Application.Services;
+
+///
+/// Registry interface for MCP Tools
+/// Manages tool discovery and dispatching
+///
+public interface IMcpToolRegistry
+{
+ ///
+ /// Get all registered tools
+ ///
+ IEnumerable GetAllTools();
+
+ ///
+ /// Get tool by name
+ ///
+ IMcpTool? GetTool(string toolName);
+
+ ///
+ /// Check if tool exists
+ ///
+ bool HasTool(string toolName);
+
+ ///
+ /// Execute a tool by name
+ ///
+ Task ExecuteToolAsync(
+ McpToolCall toolCall,
+ CancellationToken cancellationToken = default);
+}
diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/McpToolRegistry.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/McpToolRegistry.cs
new file mode 100644
index 0000000..ae92332
--- /dev/null
+++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Services/McpToolRegistry.cs
@@ -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;
+
+///
+/// Registry for MCP Tools with auto-discovery
+/// Uses constructor injection to register all IMcpTool implementations
+///
+public class McpToolRegistry : IMcpToolRegistry
+{
+ private readonly Dictionary _tools;
+ private readonly ILogger _logger;
+
+ public McpToolRegistry(
+ IEnumerable tools,
+ ILogger _logger)
+ {
+ this._logger = _logger ?? throw new ArgumentNullException(nameof(_logger));
+
+ // Auto-discover and register all tools
+ _tools = new Dictionary(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 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 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
+ };
+ }
+ }
+}
diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/AddCommentTool.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/AddCommentTool.cs
new file mode 100644
index 0000000..0e68e96
--- /dev/null
+++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/AddCommentTool.cs
@@ -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;
+
+///
+/// MCP Tool: add_comment
+/// Adds a comment to an existing Issue
+/// Generates a Diff Preview and creates a PendingChange for approval
+///
+public class AddCommentTool : IMcpTool
+{
+ private readonly IPendingChangeService _pendingChangeService;
+ private readonly IIssueRepository _issueRepository;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly DiffPreviewService _diffPreviewService;
+ private readonly ILogger _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
+ {
+ ["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 { "issueId", "content" }
+ };
+
+ public AddCommentTool(
+ IPendingChangeService pendingChangeService,
+ IIssueRepository issueRepository,
+ IHttpContextAccessor httpContextAccessor,
+ DiffPreviewService diffPreviewService,
+ ILogger 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 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
+ };
+ }
+ }
+}
diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/CreateIssueTool.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/CreateIssueTool.cs
new file mode 100644
index 0000000..3309b4e
--- /dev/null
+++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/CreateIssueTool.cs
@@ -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;
+
+///
+/// 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 : IMcpTool
+{
+ private readonly IPendingChangeService _pendingChangeService;
+ private readonly IProjectRepository _projectRepository;
+ private readonly ITenantContext _tenantContext;
+ private readonly DiffPreviewService _diffPreviewService;
+ private readonly ILogger _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 CreateIssueTool(
+ IPendingChangeService pendingChangeService,
+ IProjectRepository projectRepository,
+ ITenantContext tenantContext,
+ DiffPreviewService diffPreviewService,
+ ILogger 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 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
+ };
+ }
+ }
+}
diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/UpdateStatusTool.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/UpdateStatusTool.cs
new file mode 100644
index 0000000..337db71
--- /dev/null
+++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/UpdateStatusTool.cs
@@ -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;
+
+///
+/// MCP Tool: update_status
+/// Updates the status of an existing Issue
+/// Generates a Diff Preview and creates a PendingChange for approval
+///
+public class UpdateStatusTool : IMcpTool
+{
+ private readonly IPendingChangeService _pendingChangeService;
+ private readonly IIssueRepository _issueRepository;
+ private readonly DiffPreviewService _diffPreviewService;
+ private readonly ILogger _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
+ {
+ ["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 { "issueId", "newStatus" }
+ };
+
+ public UpdateStatusTool(
+ IPendingChangeService pendingChangeService,
+ IIssueRepository issueRepository,
+ DiffPreviewService diffPreviewService,
+ ILogger 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 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(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
+ };
+ }
+ }
+}
diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/Validation/ToolParameterParser.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/Validation/ToolParameterParser.cs
new file mode 100644
index 0000000..160b9a1
--- /dev/null
+++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/Tools/Validation/ToolParameterParser.cs
@@ -0,0 +1,334 @@
+using System.Text.Json;
+using ColaFlow.Modules.Mcp.Domain.Exceptions;
+
+namespace ColaFlow.Modules.Mcp.Application.Tools.Validation;
+
+///
+/// Helper class for parsing and validating tool parameters
+///
+public static class ToolParameterParser
+{
+ ///
+ /// Parse a required string parameter
+ ///
+ public static string ParseString(Dictionary 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;
+ }
+
+ ///
+ /// Parse a required Guid parameter
+ ///
+ public static Guid? ParseGuid(Dictionary 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");
+ }
+
+ ///
+ /// Parse an enum parameter
+ ///
+ public static TEnum? ParseEnum(Dictionary 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(strValue, ignoreCase: true, out var enumValue))
+ return enumValue;
+
+ var validValues = string.Join(", ", Enum.GetNames());
+ 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(stringValue, ignoreCase: true, out var result))
+ return result;
+
+ var validValuesList = string.Join(", ", Enum.GetNames());
+ throw new McpInvalidParamsException(
+ $"Parameter '{paramName}' must be one of: {validValuesList}");
+ }
+
+ ///
+ /// Parse a decimal parameter
+ ///
+ public static decimal? ParseDecimal(Dictionary 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");
+ }
+ }
+
+ ///
+ /// Parse an integer parameter
+ ///
+ public static int? ParseInt(Dictionary 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");
+ }
+ }
+
+ ///
+ /// Parse a boolean parameter
+ ///
+ public static bool? ParseBool(Dictionary 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");
+ }
+ }
+}
diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Contracts/Tools/IMcpTool.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Contracts/Tools/IMcpTool.cs
new file mode 100644
index 0000000..ae55150
--- /dev/null
+++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Contracts/Tools/IMcpTool.cs
@@ -0,0 +1,49 @@
+namespace ColaFlow.Modules.Mcp.Contracts.Tools;
+
+///
+/// Interface for MCP Tools
+/// Tools provide write operations to AI agents through the MCP protocol
+/// All write operations create PendingChanges and require human approval
+///
+public interface IMcpTool
+{
+ ///
+ /// Tool name (e.g., "create_issue", "update_status")
+ /// Must be unique and follow snake_case naming convention
+ ///
+ string Name { get; }
+
+ ///
+ /// Human-readable tool description for AI to understand when to use this tool
+ ///
+ string Description { get; }
+
+ ///
+ /// JSON Schema describing the tool's input parameters
+ ///
+ McpToolInputSchema InputSchema { get; }
+
+ ///
+ /// Execute the tool with the provided arguments
+ /// This should create a PendingChange, NOT execute the change directly
+ ///
+ /// The tool call request with arguments
+ /// Cancellation token
+ /// Tool execution result
+ Task ExecuteAsync(
+ McpToolCall toolCall,
+ CancellationToken cancellationToken);
+
+ ///
+ /// Get tool descriptor with full metadata
+ ///
+ McpToolDescriptor GetDescriptor()
+ {
+ return new McpToolDescriptor
+ {
+ Name = Name,
+ Description = Description,
+ InputSchema = InputSchema
+ };
+ }
+}
diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Contracts/Tools/McpToolCall.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Contracts/Tools/McpToolCall.cs
new file mode 100644
index 0000000..8db2e85
--- /dev/null
+++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Contracts/Tools/McpToolCall.cs
@@ -0,0 +1,18 @@
+namespace ColaFlow.Modules.Mcp.Contracts.Tools;
+
+///
+/// Represents a tool call request from an AI agent
+///
+public sealed class McpToolCall
+{
+ ///
+ /// Tool name to execute
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Tool arguments as key-value pairs
+ /// Values can be strings, numbers, booleans, arrays, or objects
+ ///
+ public Dictionary Arguments { get; set; } = new();
+}
diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Contracts/Tools/McpToolDescriptor.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Contracts/Tools/McpToolDescriptor.cs
new file mode 100644
index 0000000..f8a6ef7
--- /dev/null
+++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Contracts/Tools/McpToolDescriptor.cs
@@ -0,0 +1,22 @@
+namespace ColaFlow.Modules.Mcp.Contracts.Tools;
+
+///
+/// Descriptor for an MCP Tool containing metadata and schema
+///
+public sealed class McpToolDescriptor
+{
+ ///
+ /// Tool name
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Tool description
+ ///
+ public string Description { get; set; } = string.Empty;
+
+ ///
+ /// Input parameter schema
+ ///
+ public McpToolInputSchema InputSchema { get; set; } = new();
+}
diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Contracts/Tools/McpToolInputSchema.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Contracts/Tools/McpToolInputSchema.cs
new file mode 100644
index 0000000..d38d06e
--- /dev/null
+++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Contracts/Tools/McpToolInputSchema.cs
@@ -0,0 +1,89 @@
+namespace ColaFlow.Modules.Mcp.Contracts.Tools;
+
+///
+/// JSON Schema for tool input parameters
+///
+public sealed class McpToolInputSchema
+{
+ ///
+ /// Schema type (always "object" for tool inputs)
+ ///
+ public string Type { get; set; } = "object";
+
+ ///
+ /// Schema properties (parameter definitions)
+ /// Key is parameter name, value is parameter schema
+ ///
+ public Dictionary Properties { get; set; } = new();
+
+ ///
+ /// List of required parameter names
+ ///
+ public List Required { get; set; } = new();
+}
+
+///
+/// JSON Schema property definition
+///
+public sealed class JsonSchemaProperty
+{
+ ///
+ /// Property type: "string", "number", "integer", "boolean", "array", "object"
+ ///
+ public string Type { get; set; } = "string";
+
+ ///
+ /// Property description (for AI to understand)
+ ///
+ public string? Description { get; set; }
+
+ ///
+ /// Enum values (for restricted choices)
+ ///
+ public string[]? Enum { get; set; }
+
+ ///
+ /// String format hint: "uuid", "email", "date-time", "uri", etc.
+ ///
+ public string? Format { get; set; }
+
+ ///
+ /// Minimum value (for numbers)
+ ///
+ public decimal? Minimum { get; set; }
+
+ ///
+ /// Maximum value (for numbers)
+ ///
+ public decimal? Maximum { get; set; }
+
+ ///
+ /// Minimum length (for strings)
+ ///
+ public int? MinLength { get; set; }
+
+ ///
+ /// Maximum length (for strings)
+ ///
+ public int? MaxLength { get; set; }
+
+ ///
+ /// Pattern (regex) for string validation
+ ///
+ public string? Pattern { get; set; }
+
+ ///
+ /// Items schema (for arrays)
+ ///
+ public JsonSchemaProperty? Items { get; set; }
+
+ ///
+ /// Properties schema (for nested objects)
+ ///
+ public Dictionary? Properties { get; set; }
+
+ ///
+ /// Default value
+ ///
+ public object? Default { get; set; }
+}
diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Contracts/Tools/McpToolResult.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Contracts/Tools/McpToolResult.cs
new file mode 100644
index 0000000..be64684
--- /dev/null
+++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Contracts/Tools/McpToolResult.cs
@@ -0,0 +1,38 @@
+namespace ColaFlow.Modules.Mcp.Contracts.Tools;
+
+///
+/// Result of a tool execution
+///
+public sealed class McpToolResult
+{
+ ///
+ /// Tool result content (typically text describing the PendingChange created)
+ ///
+ public IEnumerable Content { get; set; } = Array.Empty();
+
+ ///
+ /// Whether the tool execution failed
+ ///
+ public bool IsError { get; set; }
+}
+
+///
+/// Content item in a tool result
+///
+public sealed class McpToolContent
+{
+ ///
+ /// Content type: "text" or "resource"
+ ///
+ public string Type { get; set; } = "text";
+
+ ///
+ /// Text content (for type="text")
+ ///
+ public string? Text { get; set; }
+
+ ///
+ /// Resource URI (for type="resource")
+ ///
+ public string? Resource { get; set; }
+}