diff --git a/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/SdkTools/CreateProjectSdkTool.cs b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/SdkTools/CreateProjectSdkTool.cs
new file mode 100644
index 0000000..f3056aa
--- /dev/null
+++ b/colaflow-api/src/Modules/Mcp/ColaFlow.Modules.Mcp.Application/SdkTools/CreateProjectSdkTool.cs
@@ -0,0 +1,167 @@
+using System.ComponentModel;
+using System.Security.Claims;
+using ColaFlow.Modules.Mcp.Application.DTOs;
+using ColaFlow.Modules.Mcp.Application.Services;
+using ColaFlow.Modules.Mcp.Domain.Exceptions;
+using ColaFlow.Modules.Mcp.Domain.Services;
+using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
+using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using ModelContextProtocol.Server;
+
+namespace ColaFlow.Modules.Mcp.Application.SdkTools;
+
+///
+/// MCP Tool: create_project (SDK-based implementation)
+/// Creates a new Project in ColaFlow
+/// Generates a Diff Preview and creates a PendingChange for approval
+///
+[McpServerToolType]
+public class CreateProjectSdkTool
+{
+ private readonly IPendingChangeService _pendingChangeService;
+ private readonly IProjectRepository _projectRepository;
+ private readonly ITenantContext _tenantContext;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+ private readonly DiffPreviewService _diffPreviewService;
+ private readonly ILogger _logger;
+
+ public CreateProjectSdkTool(
+ IPendingChangeService pendingChangeService,
+ IProjectRepository projectRepository,
+ ITenantContext tenantContext,
+ IHttpContextAccessor httpContextAccessor,
+ 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));
+ _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
+ _diffPreviewService = diffPreviewService ?? throw new ArgumentNullException(nameof(diffPreviewService));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ [McpServerTool]
+ [Description("Create a new project in ColaFlow. Projects organize issues (Epics, Stories, Tasks, Bugs). Requires human approval before being created.")]
+ public async Task CreateProjectAsync(
+ [Description("The name of the project (max 100 characters)")] string name,
+ [Description("The project key (e.g., 'CFD', 'PRJ'). Must be 2-10 uppercase letters and unique within the tenant.")] string key,
+ [Description("Detailed project description (optional, max 500 characters)")] string? description = null,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ _logger.LogInformation("Executing create_project tool (SDK)");
+
+ // 1. Validate input
+ if (string.IsNullOrWhiteSpace(name))
+ throw new McpInvalidParamsException("Project name cannot be empty");
+
+ if (name.Length > 100)
+ throw new McpInvalidParamsException("Project name cannot exceed 100 characters");
+
+ if (string.IsNullOrWhiteSpace(key))
+ throw new McpInvalidParamsException("Project key cannot be empty");
+
+ if (key.Length < 2 || key.Length > 10)
+ throw new McpInvalidParamsException("Project key must be between 2 and 10 characters");
+
+ if (!System.Text.RegularExpressions.Regex.IsMatch(key, "^[A-Z]+$"))
+ throw new McpInvalidParamsException("Project key must contain only uppercase letters");
+
+ if (description?.Length > 500)
+ throw new McpInvalidParamsException("Project description cannot exceed 500 characters");
+
+ // 2. Check if project key already exists
+ var existingProject = await _projectRepository.GetByKeyAsync(key, cancellationToken);
+ if (existingProject != null)
+ throw new McpInvalidParamsException($"Project with key '{key}' already exists");
+
+ // 3. Get Owner ID from HTTP context claims
+ var ownerId = GetUserIdFromClaims();
+
+ // 4. Get Tenant ID from context
+ var tenantId = _tenantContext.GetCurrentTenantId();
+
+ // 5. Build "after data" object for diff preview
+ var afterData = new
+ {
+ name = name,
+ key = key,
+ description = description ?? string.Empty,
+ ownerId = ownerId,
+ tenantId = tenantId,
+ status = "Active"
+ };
+
+ // 6. Generate Diff Preview (CREATE operation)
+ var diff = _diffPreviewService.GenerateCreateDiff(
+ entityType: "Project",
+ afterEntity: afterData,
+ entityKey: key // Use project key as the entity key
+ );
+
+ // 7. Create PendingChange (do NOT execute yet)
+ var pendingChange = await _pendingChangeService.CreateAsync(
+ new CreatePendingChangeRequest
+ {
+ ToolName = "create_project",
+ Diff = diff,
+ ExpirationHours = 24
+ },
+ cancellationToken);
+
+ _logger.LogInformation(
+ "PendingChange created: {PendingChangeId} - CREATE Project: {Name} ({Key})",
+ pendingChange.Id, name, key);
+
+ // 8. Return pendingChangeId to AI (NOT the created project)
+ return $"Project creation request submitted for approval.\n\n" +
+ $"**Pending Change ID**: {pendingChange.Id}\n" +
+ $"**Status**: Pending Approval\n" +
+ $"**Project Name**: {name}\n" +
+ $"**Project Key**: {key}\n" +
+ $"**Description**: {(string.IsNullOrEmpty(description) ? "(none)" : description)}\n\n" +
+ $"A human user must approve this change before the project is created. " +
+ $"The change will expire at {pendingChange.ExpiresAt:yyyy-MM-dd HH:mm} UTC if not approved.";
+ }
+ catch (McpException)
+ {
+ throw; // Re-throw MCP exceptions as-is
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error executing create_project tool (SDK)");
+ throw new McpInvalidParamsException($"Error creating project: {ex.Message}");
+ }
+ }
+
+ private Guid GetUserIdFromClaims()
+ {
+ var httpContext = _httpContextAccessor.HttpContext;
+ if (httpContext == null)
+ throw new McpInvalidParamsException("HTTP context not available");
+
+ var userIdClaim = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value
+ ?? httpContext.User.FindFirst("sub")?.Value;
+
+ if (string.IsNullOrEmpty(userIdClaim))
+ {
+ // Fallback: Try to get from API key context
+ var apiKeyId = httpContext.Items["ApiKeyId"] as Guid?;
+ if (apiKeyId.HasValue)
+ {
+ // Use API key ID as owner ID (for MCP API key authentication)
+ return apiKeyId.Value;
+ }
+ throw new McpInvalidParamsException("User ID not found in authentication context");
+ }
+
+ if (!Guid.TryParse(userIdClaim, out var userId))
+ throw new McpInvalidParamsException("Invalid user ID format in authentication context");
+
+ return userId;
+ }
+}