Clean up
This commit is contained in:
105
colaflow-api/scripts/explore-mcp-sdk.csx
Normal file
105
colaflow-api/scripts/explore-mcp-sdk.csx
Normal file
@@ -0,0 +1,105 @@
|
||||
// C# Script to explore ModelContextProtocol SDK APIs
|
||||
#r "nuget: ModelContextProtocol, 0.4.0-preview.3"
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
|
||||
// Load the ModelContextProtocol assembly
|
||||
var mcpAssembly = Assembly.Load("ModelContextProtocol");
|
||||
|
||||
Console.WriteLine("=== ModelContextProtocol SDK API Exploration ===");
|
||||
Console.WriteLine($"Assembly: {mcpAssembly.FullName}");
|
||||
Console.WriteLine();
|
||||
|
||||
// Get all public types
|
||||
var types = mcpAssembly.GetExportedTypes()
|
||||
.OrderBy(t => t.Namespace)
|
||||
.ThenBy(t => t.Name);
|
||||
|
||||
Console.WriteLine($"Total Public Types: {types.Count()}");
|
||||
Console.WriteLine();
|
||||
|
||||
// Group by namespace
|
||||
var namespaces = types.GroupBy(t => t.Namespace ?? "No Namespace");
|
||||
|
||||
foreach (var ns in namespaces)
|
||||
{
|
||||
Console.WriteLine($"\n### Namespace: {ns.Key}");
|
||||
Console.WriteLine(new string('-', 60));
|
||||
|
||||
foreach (var type in ns)
|
||||
{
|
||||
var typeKind = type.IsInterface ? "interface" :
|
||||
type.IsClass && type.IsAbstract ? "abstract class" :
|
||||
type.IsClass ? "class" :
|
||||
type.IsEnum ? "enum" :
|
||||
type.IsValueType ? "struct" : "type";
|
||||
|
||||
Console.WriteLine($" [{typeKind}] {type.Name}");
|
||||
|
||||
// Show attributes
|
||||
var attrs = type.GetCustomAttributes(false);
|
||||
if (attrs.Any())
|
||||
{
|
||||
foreach (var attr in attrs)
|
||||
{
|
||||
Console.WriteLine($" @{attr.GetType().Name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Look for specific patterns
|
||||
Console.WriteLine("\n\n=== Looking for MCP-Specific Patterns ===");
|
||||
Console.WriteLine(new string('-', 60));
|
||||
|
||||
// Look for Tool-related types
|
||||
var toolTypes = types.Where(t => t.Name.Contains("Tool", StringComparison.OrdinalIgnoreCase));
|
||||
Console.WriteLine($"\nTool-related types ({toolTypes.Count()}):");
|
||||
foreach (var t in toolTypes)
|
||||
{
|
||||
Console.WriteLine($" - {t.FullName}");
|
||||
}
|
||||
|
||||
// Look for Resource-related types
|
||||
var resourceTypes = types.Where(t => t.Name.Contains("Resource", StringComparison.OrdinalIgnoreCase));
|
||||
Console.WriteLine($"\nResource-related types ({resourceTypes.Count()}):");
|
||||
foreach (var t in resourceTypes)
|
||||
{
|
||||
Console.WriteLine($" - {t.FullName}");
|
||||
}
|
||||
|
||||
// Look for Attribute types
|
||||
var attributeTypes = types.Where(t => t.Name.EndsWith("Attribute", StringComparison.OrdinalIgnoreCase));
|
||||
Console.WriteLine($"\nAttribute types ({attributeTypes.Count()}):");
|
||||
foreach (var t in attributeTypes)
|
||||
{
|
||||
Console.WriteLine($" - {t.Name}");
|
||||
}
|
||||
|
||||
// Look for Server-related types
|
||||
var serverTypes = types.Where(t => t.Name.Contains("Server", StringComparison.OrdinalIgnoreCase));
|
||||
Console.WriteLine($"\nServer-related types ({serverTypes.Count()}):");
|
||||
foreach (var t in serverTypes)
|
||||
{
|
||||
Console.WriteLine($" - {t.FullName}");
|
||||
}
|
||||
|
||||
// Look for Client-related types
|
||||
var clientTypes = types.Where(t => t.Name.Contains("Client", StringComparison.OrdinalIgnoreCase));
|
||||
Console.WriteLine($"\nClient-related types ({clientTypes.Count()}):");
|
||||
foreach (var t in clientTypes)
|
||||
{
|
||||
Console.WriteLine($" - {t.FullName}");
|
||||
}
|
||||
|
||||
// Look for Transport-related types
|
||||
var transportTypes = types.Where(t => t.Name.Contains("Transport", StringComparison.OrdinalIgnoreCase));
|
||||
Console.WriteLine($"\nTransport-related types ({transportTypes.Count()}):");
|
||||
foreach (var t in transportTypes)
|
||||
{
|
||||
Console.WriteLine($" - {t.FullName}");
|
||||
}
|
||||
|
||||
Console.WriteLine("\n=== Exploration Complete ===");
|
||||
@@ -14,6 +14,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="ModelContextProtocol" Version="0.4.0-preview.3" />
|
||||
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.4.0-preview.3" />
|
||||
<PackageReference Include="Scalar.AspNetCore" Version="2.9.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
26
colaflow-api/src/ColaFlow.API/McpSdkExplorer.cs
Normal file
26
colaflow-api/src/ColaFlow.API/McpSdkExplorer.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
// Temporary file to explore ModelContextProtocol SDK APIs
|
||||
// This file will be deleted after exploration
|
||||
|
||||
using ModelContextProtocol;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace ColaFlow.API.Exploration;
|
||||
|
||||
/// <summary>
|
||||
/// Temporary class to explore ModelContextProtocol SDK APIs
|
||||
/// </summary>
|
||||
public class McpSdkExplorer
|
||||
{
|
||||
public void ExploreServices(IServiceCollection services)
|
||||
{
|
||||
// Try to discover SDK extension methods
|
||||
// services.AddMcp...
|
||||
// services.AddModelContext...
|
||||
}
|
||||
|
||||
public void ExploreTypes()
|
||||
{
|
||||
// List all types we can discover
|
||||
// var type = typeof(???);
|
||||
}
|
||||
}
|
||||
@@ -45,15 +45,17 @@ builder.Services.AddIssueManagementModule(builder.Configuration, builder.Environ
|
||||
builder.Services.AddIdentityApplication();
|
||||
builder.Services.AddIdentityInfrastructure(builder.Configuration, builder.Environment);
|
||||
|
||||
// Register MCP Module (Custom Implementation)
|
||||
// Register MCP Module (Custom Implementation - Keep for Diff Preview services)
|
||||
builder.Services.AddMcpModule(builder.Configuration);
|
||||
|
||||
// ============================================
|
||||
// Register Microsoft MCP SDK (PoC - Phase 1)
|
||||
// Register Microsoft MCP SDK (Official)
|
||||
// ============================================
|
||||
builder.Services.AddMcpServer()
|
||||
.WithToolsFromAssembly() // Auto-discover tools with [McpServerToolType] attribute
|
||||
.WithResourcesFromAssembly(); // Auto-discover resources with [McpServerResourceType] attribute
|
||||
.WithHttpTransport() // Required for MapMcp() endpoint
|
||||
.WithToolsFromAssembly(typeof(ColaFlow.Modules.Mcp.Application.SdkTools.CreateIssueSdkTool).Assembly)
|
||||
.WithResourcesFromAssembly(typeof(ColaFlow.Modules.Mcp.Application.SdkResources.ProjectsSdkResource).Assembly)
|
||||
.WithPromptsFromAssembly(typeof(ColaFlow.Modules.Mcp.Application.SdkPrompts.ProjectManagementPrompts).Assembly);
|
||||
|
||||
// Add Response Caching
|
||||
builder.Services.AddResponseCaching();
|
||||
@@ -235,6 +237,12 @@ app.MapHub<ProjectHub>("/hubs/project");
|
||||
app.MapHub<NotificationHub>("/hubs/notification");
|
||||
app.MapHub<McpNotificationHub>("/hubs/mcp-notifications");
|
||||
|
||||
// ============================================
|
||||
// Map MCP SDK Endpoint
|
||||
// ============================================
|
||||
app.MapMcp("/mcp-sdk"); // Official SDK endpoint at /mcp-sdk
|
||||
// Note: Legacy /mcp endpoint still handled by UseMcpMiddleware() above
|
||||
|
||||
// ============================================
|
||||
// Auto-migrate databases in development
|
||||
// ============================================
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
|
||||
<PackageReference Include="ModelContextProtocol" Version="0.4.0-preview.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
using System.ComponentModel;
|
||||
using Microsoft.Extensions.AI;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ColaFlow.Modules.Mcp.Application.SdkPrompts;
|
||||
|
||||
/// <summary>
|
||||
/// MCP Prompts for project management tasks
|
||||
/// Provides pre-defined prompt templates for AI interactions
|
||||
/// </summary>
|
||||
[McpServerPromptType]
|
||||
public static class ProjectManagementPrompts
|
||||
{
|
||||
[McpServerPrompt]
|
||||
[Description("Generate a Product Requirements Document (PRD) for an Epic")]
|
||||
public static ChatMessage GeneratePrdPrompt(
|
||||
[Description("The Epic title")] string epicTitle,
|
||||
[Description("Brief description of the Epic")] string epicDescription)
|
||||
{
|
||||
var promptText = $@"You are a Product Manager creating a Product Requirements Document (PRD).
|
||||
|
||||
**Epic**: {epicTitle}
|
||||
**Description**: {epicDescription}
|
||||
|
||||
Please create a comprehensive PRD that includes:
|
||||
|
||||
1. **Executive Summary**
|
||||
- Brief overview of the feature
|
||||
- Business value and goals
|
||||
|
||||
2. **User Stories**
|
||||
- Who are the users?
|
||||
- What problems does this solve?
|
||||
|
||||
3. **Functional Requirements**
|
||||
- List all key features
|
||||
- User workflows and interactions
|
||||
|
||||
4. **Non-Functional Requirements**
|
||||
- Performance expectations
|
||||
- Security considerations
|
||||
- Scalability needs
|
||||
|
||||
5. **Acceptance Criteria**
|
||||
- Clear, testable criteria for completion
|
||||
- Success metrics
|
||||
|
||||
6. **Technical Considerations**
|
||||
- API requirements
|
||||
- Data models
|
||||
- Integration points
|
||||
|
||||
7. **Timeline and Milestones**
|
||||
- Estimated timeline
|
||||
- Key milestones
|
||||
- Dependencies
|
||||
|
||||
Please format the PRD in Markdown.";
|
||||
|
||||
return new ChatMessage(ChatRole.User, promptText);
|
||||
}
|
||||
|
||||
[McpServerPrompt]
|
||||
[Description("Break down an Epic into smaller Stories")]
|
||||
public static ChatMessage SplitEpicToStoriesPrompt(
|
||||
[Description("The Epic title")] string epicTitle,
|
||||
[Description("The Epic description or PRD")] string epicContent)
|
||||
{
|
||||
var promptText = $@"You are a Product Manager breaking down an Epic into manageable Stories.
|
||||
|
||||
**Epic**: {epicTitle}
|
||||
|
||||
**Epic Content**:
|
||||
{epicContent}
|
||||
|
||||
Please break this Epic down into 5-10 User Stories following these guidelines:
|
||||
|
||||
1. **Each Story should**:
|
||||
- Be independently valuable
|
||||
- Be completable in 1-3 days
|
||||
- Follow the format: ""As a [user], I want [feature] so that [benefit]""
|
||||
- Include acceptance criteria
|
||||
|
||||
2. **Story Structure**:
|
||||
```
|
||||
**Story Title**: [Concise title]
|
||||
**User Story**: As a [user], I want [feature] so that [benefit]
|
||||
**Description**: [Detailed description]
|
||||
**Acceptance Criteria**:
|
||||
- [ ] Criterion 1
|
||||
- [ ] Criterion 2
|
||||
- [ ] Criterion 3
|
||||
**Estimated Effort**: [Small/Medium/Large]
|
||||
**Priority**: [High/Medium/Low]
|
||||
```
|
||||
|
||||
3. **Prioritize the Stories**:
|
||||
- Mark dependencies between stories
|
||||
- Suggest implementation order
|
||||
|
||||
Please output the Stories in Markdown format.";
|
||||
|
||||
return new ChatMessage(ChatRole.User, promptText);
|
||||
}
|
||||
|
||||
[McpServerPrompt]
|
||||
[Description("Generate acceptance criteria for a Story")]
|
||||
public static ChatMessage GenerateAcceptanceCriteriaPrompt(
|
||||
[Description("The Story title")] string storyTitle,
|
||||
[Description("The Story description")] string storyDescription)
|
||||
{
|
||||
var promptText = $@"You are a QA Engineer defining acceptance criteria for a User Story.
|
||||
|
||||
**Story**: {storyTitle}
|
||||
**Description**: {storyDescription}
|
||||
|
||||
Please create comprehensive acceptance criteria following these guidelines:
|
||||
|
||||
1. **Criteria should be**:
|
||||
- Specific and measurable
|
||||
- Testable (can be verified)
|
||||
- Clear and unambiguous
|
||||
- Focused on outcomes, not implementation
|
||||
|
||||
2. **Include**:
|
||||
- Functional acceptance criteria (what the feature does)
|
||||
- Non-functional acceptance criteria (performance, security, UX)
|
||||
- Edge cases and error scenarios
|
||||
|
||||
3. **Format**:
|
||||
```
|
||||
**Given**: [Initial context/state]
|
||||
**When**: [Action taken]
|
||||
**Then**: [Expected outcome]
|
||||
```
|
||||
|
||||
Please output 5-10 acceptance criteria in Markdown format.";
|
||||
|
||||
return new ChatMessage(ChatRole.User, promptText);
|
||||
}
|
||||
|
||||
[McpServerPrompt]
|
||||
[Description("Analyze Sprint progress and provide insights")]
|
||||
public static ChatMessage AnalyzeSprintProgressPrompt(
|
||||
[Description("Sprint name")] string sprintName,
|
||||
[Description("Sprint data (JSON format)")] string sprintData)
|
||||
{
|
||||
var promptText = $@"You are a Scrum Master analyzing Sprint progress.
|
||||
|
||||
**Sprint**: {sprintName}
|
||||
|
||||
**Sprint Data**:
|
||||
```json
|
||||
{sprintData}
|
||||
```
|
||||
|
||||
Please analyze the Sprint and provide:
|
||||
|
||||
1. **Progress Summary**:
|
||||
- Overall completion percentage
|
||||
- Story points completed vs. planned
|
||||
- Burndown trend analysis
|
||||
|
||||
2. **Risk Assessment**:
|
||||
- Tasks at risk of not completing
|
||||
- Blockers and bottlenecks
|
||||
- Velocity concerns
|
||||
|
||||
3. **Recommendations**:
|
||||
- Actions to get back on track
|
||||
- Tasks that could be descoped
|
||||
- Resource allocation suggestions
|
||||
|
||||
4. **Team Health**:
|
||||
- Workload distribution
|
||||
- Identify overloaded team members
|
||||
- Suggest load balancing
|
||||
|
||||
Please format the analysis in Markdown with clear sections.";
|
||||
|
||||
return new ChatMessage(ChatRole.User, promptText);
|
||||
}
|
||||
|
||||
[McpServerPrompt]
|
||||
[Description("Generate a Sprint retrospective summary")]
|
||||
public static ChatMessage GenerateRetrospectivePrompt(
|
||||
[Description("Sprint name")] string sprintName,
|
||||
[Description("Sprint completion data")] string sprintData,
|
||||
[Description("Team feedback (optional)")] string? teamFeedback = null)
|
||||
{
|
||||
var feedbackSection = string.IsNullOrEmpty(teamFeedback)
|
||||
? ""
|
||||
: $@"
|
||||
|
||||
**Team Feedback**:
|
||||
{teamFeedback}";
|
||||
|
||||
var promptText = $@"You are a Scrum Master facilitating a Sprint Retrospective.
|
||||
|
||||
**Sprint**: {sprintName}
|
||||
|
||||
**Sprint Data**:
|
||||
```json
|
||||
{sprintData}
|
||||
```
|
||||
{feedbackSection}
|
||||
|
||||
Please create a comprehensive retrospective summary using the format:
|
||||
|
||||
1. **What Went Well** 🎉
|
||||
- Successes and achievements
|
||||
- Team highlights
|
||||
|
||||
2. **What Didn't Go Well** 😞
|
||||
- Challenges faced
|
||||
- Missed goals
|
||||
- Technical issues
|
||||
|
||||
3. **Lessons Learned** 📚
|
||||
- Key takeaways
|
||||
- Insights gained
|
||||
|
||||
4. **Action Items** 🎯
|
||||
- Specific, actionable improvements
|
||||
- Owner for each action
|
||||
- Target date
|
||||
|
||||
5. **Metrics** 📊
|
||||
- Velocity achieved
|
||||
- Story points completed
|
||||
- Sprint goal achievement
|
||||
|
||||
Please format the retrospective in Markdown.";
|
||||
|
||||
return new ChatMessage(ChatRole.User, promptText);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using ColaFlow.Modules.Mcp.Domain.Exceptions;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ColaFlow.Modules.Mcp.Application.SdkResources;
|
||||
|
||||
/// <summary>
|
||||
/// MCP Resource: Issues (SDK-based implementation)
|
||||
/// Provides search and get functionality for Issues (Epics, Stories, Tasks)
|
||||
/// </summary>
|
||||
[McpServerResourceType]
|
||||
public class IssuesSdkResource
|
||||
{
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly ITenantContext _tenantContext;
|
||||
private readonly ILogger<IssuesSdkResource> _logger;
|
||||
|
||||
public IssuesSdkResource(
|
||||
IProjectRepository projectRepository,
|
||||
ITenantContext tenantContext,
|
||||
ILogger<IssuesSdkResource> logger)
|
||||
{
|
||||
_projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||
_tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
[McpServerResource]
|
||||
[Description("Search issues with filters (status, priority, assignee, type)")]
|
||||
public async Task<string> SearchIssuesAsync(
|
||||
[Description("Filter by project ID (optional)")] Guid? project = null,
|
||||
[Description("Filter by status (optional)")] string? status = null,
|
||||
[Description("Filter by priority (optional)")] string? priority = null,
|
||||
[Description("Filter by type: epic, story, or task (optional)")] string? type = null,
|
||||
[Description("Filter by assignee ID (optional)")] Guid? assignee = null,
|
||||
[Description("Maximum number of results (default: 100)")] int limit = 100,
|
||||
[Description("Offset for pagination (default: 0)")] int offset = 0,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = _tenantContext.GetCurrentTenantId();
|
||||
|
||||
_logger.LogDebug("Searching issues for tenant {TenantId} (SDK)", tenantId);
|
||||
|
||||
// Limit max results
|
||||
limit = Math.Min(limit, 100);
|
||||
|
||||
// Get projects
|
||||
var projects = await _projectRepository.GetAllProjectsReadOnlyAsync(cancellationToken);
|
||||
|
||||
// Filter by project if specified
|
||||
if (project.HasValue)
|
||||
{
|
||||
var projectId = ProjectId.From(project.Value);
|
||||
var singleProject = await _projectRepository.GetProjectWithFullHierarchyReadOnlyAsync(projectId, cancellationToken);
|
||||
projects = singleProject != null ? new List<ProjectManagement.Domain.Aggregates.ProjectAggregate.Project> { singleProject } : new();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Load full hierarchy for all projects
|
||||
var projectsWithHierarchy = new List<ProjectManagement.Domain.Aggregates.ProjectAggregate.Project>();
|
||||
foreach (var p in projects)
|
||||
{
|
||||
var fullProject = await _projectRepository.GetProjectWithFullHierarchyReadOnlyAsync(p.Id, cancellationToken);
|
||||
if (fullProject != null)
|
||||
{
|
||||
projectsWithHierarchy.Add(fullProject);
|
||||
}
|
||||
}
|
||||
projects = projectsWithHierarchy;
|
||||
}
|
||||
|
||||
// Collect all issues
|
||||
var allIssues = new List<object>();
|
||||
|
||||
foreach (var proj in projects)
|
||||
{
|
||||
if (proj.Epics == null) continue;
|
||||
|
||||
foreach (var epic in proj.Epics)
|
||||
{
|
||||
// Filter Epics
|
||||
if (ShouldIncludeIssue("epic", type, epic.Status.ToString(), status,
|
||||
epic.Priority.ToString(), priority, null, assignee?.ToString()))
|
||||
{
|
||||
allIssues.Add(new
|
||||
{
|
||||
id = epic.Id.Value,
|
||||
type = "Epic",
|
||||
name = epic.Name,
|
||||
description = epic.Description,
|
||||
status = epic.Status.ToString(),
|
||||
priority = epic.Priority.ToString(),
|
||||
projectId = proj.Id.Value,
|
||||
projectName = proj.Name,
|
||||
createdAt = epic.CreatedAt,
|
||||
storyCount = epic.Stories?.Count ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
// Filter Stories
|
||||
if (epic.Stories != null)
|
||||
{
|
||||
foreach (var story in epic.Stories)
|
||||
{
|
||||
if (ShouldIncludeIssue("story", type, story.Status.ToString(), status,
|
||||
story.Priority.ToString(), priority, story.AssigneeId?.Value.ToString(), assignee?.ToString()))
|
||||
{
|
||||
allIssues.Add(new
|
||||
{
|
||||
id = story.Id.Value,
|
||||
type = "Story",
|
||||
title = story.Title,
|
||||
description = story.Description,
|
||||
status = story.Status.ToString(),
|
||||
priority = story.Priority.ToString(),
|
||||
assigneeId = story.AssigneeId?.Value,
|
||||
projectId = proj.Id.Value,
|
||||
projectName = proj.Name,
|
||||
epicId = epic.Id.Value,
|
||||
epicName = epic.Name,
|
||||
createdAt = story.CreatedAt,
|
||||
taskCount = story.Tasks?.Count ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
// Filter Tasks
|
||||
if (story.Tasks != null)
|
||||
{
|
||||
foreach (var task in story.Tasks)
|
||||
{
|
||||
if (ShouldIncludeIssue("task", type, task.Status.ToString(), status,
|
||||
task.Priority.ToString(), priority, task.AssigneeId?.Value.ToString(), assignee?.ToString()))
|
||||
{
|
||||
allIssues.Add(new
|
||||
{
|
||||
id = task.Id.Value,
|
||||
type = "Task",
|
||||
title = task.Title,
|
||||
description = task.Description,
|
||||
status = task.Status.ToString(),
|
||||
priority = task.Priority.ToString(),
|
||||
assigneeId = task.AssigneeId?.Value,
|
||||
projectId = proj.Id.Value,
|
||||
projectName = proj.Name,
|
||||
storyId = story.Id.Value,
|
||||
storyTitle = story.Title,
|
||||
epicId = epic.Id.Value,
|
||||
epicName = epic.Name,
|
||||
createdAt = task.CreatedAt
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
var total = allIssues.Count;
|
||||
var paginatedIssues = allIssues.Skip(offset).Take(limit).ToList();
|
||||
|
||||
var result = JsonSerializer.Serialize(new
|
||||
{
|
||||
issues = paginatedIssues,
|
||||
total = total,
|
||||
limit = limit,
|
||||
offset = offset
|
||||
}, new JsonSerializerOptions { WriteIndented = true });
|
||||
|
||||
_logger.LogInformation("Found {Count} issues for tenant {TenantId} (SDK, total: {Total})",
|
||||
paginatedIssues.Count, tenantId, total);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private bool ShouldIncludeIssue(
|
||||
string issueType,
|
||||
string? typeFilter,
|
||||
string status,
|
||||
string? statusFilter,
|
||||
string priority,
|
||||
string? priorityFilter,
|
||||
string? assigneeId,
|
||||
string? assigneeFilter)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(typeFilter) && !issueType.Equals(typeFilter, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (!string.IsNullOrEmpty(statusFilter) && !status.Equals(statusFilter, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (!string.IsNullOrEmpty(priorityFilter) && !priority.Equals(priorityFilter, StringComparison.OrdinalIgnoreCase))
|
||||
return false;
|
||||
|
||||
if (!string.IsNullOrEmpty(assigneeFilter) && assigneeId != assigneeFilter)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ColaFlow.Modules.Mcp.Application.SdkResources;
|
||||
|
||||
/// <summary>
|
||||
/// MCP Resource: Projects (SDK-based implementation)
|
||||
/// Provides access to project data in the current tenant
|
||||
/// </summary>
|
||||
[McpServerResourceType]
|
||||
public class ProjectsSdkResource
|
||||
{
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly ITenantContext _tenantContext;
|
||||
private readonly ILogger<ProjectsSdkResource> _logger;
|
||||
|
||||
public ProjectsSdkResource(
|
||||
IProjectRepository projectRepository,
|
||||
ITenantContext tenantContext,
|
||||
ILogger<ProjectsSdkResource> logger)
|
||||
{
|
||||
_projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||
_tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
[McpServerResource]
|
||||
[Description("List all projects in current tenant")]
|
||||
public async Task<string> ListProjectsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = _tenantContext.GetCurrentTenantId();
|
||||
|
||||
_logger.LogDebug("Fetching projects list for tenant {TenantId} (SDK)", tenantId);
|
||||
|
||||
// Get all projects (read-only)
|
||||
var projects = await _projectRepository.GetAllProjectsReadOnlyAsync(cancellationToken);
|
||||
|
||||
// Map to DTOs
|
||||
var projectDtos = projects.Select(p => new
|
||||
{
|
||||
id = p.Id.Value,
|
||||
name = p.Name,
|
||||
key = p.Key.ToString(),
|
||||
description = p.Description,
|
||||
status = p.Status.ToString(),
|
||||
createdAt = p.CreatedAt,
|
||||
updatedAt = p.UpdatedAt,
|
||||
epicCount = p.Epics?.Count ?? 0
|
||||
}).ToList();
|
||||
|
||||
var result = JsonSerializer.Serialize(new
|
||||
{
|
||||
projects = projectDtos,
|
||||
total = projectDtos.Count
|
||||
}, new JsonSerializerOptions { WriteIndented = true });
|
||||
|
||||
_logger.LogInformation("Retrieved {Count} projects for tenant {TenantId} (SDK)", projectDtos.Count, tenantId);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[McpServerResource]
|
||||
[Description("Get detailed information about a specific project")]
|
||||
public async Task<string> GetProjectAsync(
|
||||
[Description("The project ID")] Guid projectId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = _tenantContext.GetCurrentTenantId();
|
||||
|
||||
_logger.LogDebug("Fetching project {ProjectId} for tenant {TenantId} (SDK)", projectId, tenantId);
|
||||
|
||||
var project = await _projectRepository.GetByIdAsync(
|
||||
ProjectManagement.Domain.ValueObjects.ProjectId.From(projectId),
|
||||
cancellationToken);
|
||||
|
||||
if (project == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Project with ID {projectId} not found");
|
||||
}
|
||||
|
||||
var result = JsonSerializer.Serialize(new
|
||||
{
|
||||
id = project.Id.Value,
|
||||
name = project.Name,
|
||||
key = project.Key.ToString(),
|
||||
description = project.Description,
|
||||
status = project.Status.ToString(),
|
||||
createdAt = project.CreatedAt,
|
||||
updatedAt = project.UpdatedAt,
|
||||
epics = project.Epics?.Select(e => new
|
||||
{
|
||||
id = e.Id.Value,
|
||||
title = e.Name, // Epic uses Name instead of Title
|
||||
status = e.Status.ToString()
|
||||
}).ToList() ?? (object)new List<object>()
|
||||
}, new JsonSerializerOptions { WriteIndented = true });
|
||||
|
||||
_logger.LogInformation("Retrieved project {ProjectId} for tenant {TenantId} (SDK)", projectId, tenantId);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using ColaFlow.Modules.Mcp.Domain.Exceptions;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ColaFlow.Modules.Mcp.Application.SdkResources;
|
||||
|
||||
/// <summary>
|
||||
/// MCP Resource: Sprints (SDK-based implementation)
|
||||
/// Provides access to Sprint data
|
||||
/// </summary>
|
||||
[McpServerResourceType]
|
||||
public class SprintsSdkResource
|
||||
{
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly ITenantContext _tenantContext;
|
||||
private readonly ILogger<SprintsSdkResource> _logger;
|
||||
|
||||
public SprintsSdkResource(
|
||||
IProjectRepository projectRepository,
|
||||
ITenantContext tenantContext,
|
||||
ILogger<SprintsSdkResource> logger)
|
||||
{
|
||||
_projectRepository = projectRepository ?? throw new ArgumentNullException(nameof(projectRepository));
|
||||
_tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
[McpServerResource]
|
||||
[Description("Get the currently active Sprint(s)")]
|
||||
public async Task<string> GetCurrentSprintAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = _tenantContext.GetCurrentTenantId();
|
||||
|
||||
_logger.LogDebug("Fetching active sprints for tenant {TenantId} (SDK)", tenantId);
|
||||
|
||||
// Get active sprints
|
||||
var activeSprints = await _projectRepository.GetActiveSprintsAsync(cancellationToken);
|
||||
|
||||
if (activeSprints.Count == 0)
|
||||
{
|
||||
_logger.LogWarning("No active sprints found for tenant {TenantId}", tenantId);
|
||||
throw new McpNotFoundException("No active sprints found");
|
||||
}
|
||||
|
||||
// Map to DTOs with statistics
|
||||
var sprintDtos = activeSprints.Select(sprint => new
|
||||
{
|
||||
id = sprint.Id.Value,
|
||||
name = sprint.Name,
|
||||
goal = sprint.Goal,
|
||||
status = sprint.Status.ToString(),
|
||||
startDate = sprint.StartDate,
|
||||
endDate = sprint.EndDate,
|
||||
createdAt = sprint.CreatedAt,
|
||||
statistics = new
|
||||
{
|
||||
totalTasks = sprint.TaskIds?.Count ?? 0
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
var result = JsonSerializer.Serialize(new
|
||||
{
|
||||
sprints = sprintDtos,
|
||||
total = sprintDtos.Count
|
||||
}, new JsonSerializerOptions { WriteIndented = true });
|
||||
|
||||
_logger.LogInformation("Retrieved {Count} active sprints for tenant {TenantId} (SDK)",
|
||||
sprintDtos.Count, tenantId);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants;
|
||||
using ColaFlow.Modules.Identity.Domain.Repositories;
|
||||
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ColaFlow.Modules.Mcp.Application.SdkResources;
|
||||
|
||||
/// <summary>
|
||||
/// MCP Resource: Users (SDK-based implementation)
|
||||
/// Provides access to team member data
|
||||
/// </summary>
|
||||
[McpServerResourceType]
|
||||
public class UsersSdkResource
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly ITenantContext _tenantContext;
|
||||
private readonly ILogger<UsersSdkResource> _logger;
|
||||
|
||||
public UsersSdkResource(
|
||||
IUserRepository userRepository,
|
||||
ITenantContext tenantContext,
|
||||
ILogger<UsersSdkResource> logger)
|
||||
{
|
||||
_userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository));
|
||||
_tenantContext = tenantContext ?? throw new ArgumentNullException(nameof(tenantContext));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
[McpServerResource]
|
||||
[Description("List all team members in current tenant")]
|
||||
public async Task<string> ListUsersAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = _tenantContext.GetCurrentTenantId();
|
||||
|
||||
_logger.LogDebug("Fetching users list for tenant {TenantId} (SDK)", tenantId);
|
||||
|
||||
// Get all users for tenant
|
||||
var users = await _userRepository.GetAllByTenantAsync(TenantId.Create(tenantId), cancellationToken);
|
||||
|
||||
// Map to DTOs
|
||||
var userDtos = users.Select(u => new
|
||||
{
|
||||
id = u.Id,
|
||||
email = u.Email.Value,
|
||||
fullName = u.FullName.ToString(),
|
||||
status = u.Status.ToString(),
|
||||
createdAt = u.CreatedAt,
|
||||
avatarUrl = u.AvatarUrl,
|
||||
jobTitle = u.JobTitle
|
||||
}).ToList();
|
||||
|
||||
var result = JsonSerializer.Serialize(new
|
||||
{
|
||||
users = userDtos,
|
||||
total = userDtos.Count
|
||||
}, new JsonSerializerOptions { WriteIndented = true });
|
||||
|
||||
_logger.LogInformation("Retrieved {Count} users for tenant {TenantId} (SDK)", userDtos.Count, tenantId);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using System.ComponentModel;
|
||||
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.IssueManagement.Domain.Repositories;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ColaFlow.Modules.Mcp.Application.SdkTools;
|
||||
|
||||
/// <summary>
|
||||
/// MCP Tool: add_comment (SDK-based implementation)
|
||||
/// Adds a comment to an existing Issue
|
||||
/// Generates a Diff Preview and creates a PendingChange for approval
|
||||
/// </summary>
|
||||
[McpServerToolType]
|
||||
public class AddCommentSdkTool
|
||||
{
|
||||
private readonly IPendingChangeService _pendingChangeService;
|
||||
private readonly IIssueRepository _issueRepository;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly DiffPreviewService _diffPreviewService;
|
||||
private readonly ILogger<AddCommentSdkTool> _logger;
|
||||
|
||||
public AddCommentSdkTool(
|
||||
IPendingChangeService pendingChangeService,
|
||||
IIssueRepository issueRepository,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
DiffPreviewService diffPreviewService,
|
||||
ILogger<AddCommentSdkTool> 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));
|
||||
}
|
||||
|
||||
[McpServerTool]
|
||||
[Description("Add a comment to an existing issue. Supports markdown formatting. Requires human approval before being added.")]
|
||||
public async Task<string> AddCommentAsync(
|
||||
[Description("The ID of the issue to comment on")] Guid issueId,
|
||||
[Description("The comment content (supports markdown, max 2000 characters)")] string content,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Executing add_comment tool (SDK)");
|
||||
|
||||
// 1. 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 = "add_comment",
|
||||
Diff = diff,
|
||||
ExpirationHours = 24
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"PendingChange created: {PendingChangeId} - CREATE Comment on Issue {IssueId}",
|
||||
pendingChange.Id, issueId);
|
||||
|
||||
// 7. Return pendingChangeId to AI
|
||||
return $"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.";
|
||||
}
|
||||
catch (McpException)
|
||||
{
|
||||
throw; // Re-throw MCP exceptions as-is
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error executing add_comment tool (SDK)");
|
||||
throw new McpInvalidParamsException($"Error adding comment: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
using System.ComponentModel;
|
||||
using ColaFlow.Modules.Mcp.Application.DTOs;
|
||||
using ColaFlow.Modules.Mcp.Application.Services;
|
||||
using ColaFlow.Modules.Mcp.Application.Tools.Validation;
|
||||
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;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ColaFlow.Modules.Mcp.Application.SdkTools;
|
||||
|
||||
/// <summary>
|
||||
/// MCP Tool: create_issue (SDK-based implementation)
|
||||
/// Creates a new Issue (Epic, Story, Task, or Bug)
|
||||
/// Generates a Diff Preview and creates a PendingChange for approval
|
||||
/// </summary>
|
||||
[McpServerToolType]
|
||||
public class CreateIssueSdkTool
|
||||
{
|
||||
private readonly IPendingChangeService _pendingChangeService;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
private readonly ITenantContext _tenantContext;
|
||||
private readonly DiffPreviewService _diffPreviewService;
|
||||
private readonly ILogger<CreateIssueSdkTool> _logger;
|
||||
|
||||
public CreateIssueSdkTool(
|
||||
IPendingChangeService pendingChangeService,
|
||||
IProjectRepository projectRepository,
|
||||
ITenantContext tenantContext,
|
||||
DiffPreviewService diffPreviewService,
|
||||
ILogger<CreateIssueSdkTool> 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));
|
||||
}
|
||||
|
||||
[McpServerTool]
|
||||
[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 async Task<string> CreateIssueAsync(
|
||||
[Description("The ID of the project to create the issue in")] Guid projectId,
|
||||
[Description("Issue title (max 200 characters)")] string title,
|
||||
[Description("Issue type: Epic, Story, Task, or Bug")] string type,
|
||||
[Description("Detailed issue description (optional, max 2000 characters)")] string? description = null,
|
||||
[Description("Issue priority: Low, Medium, High, or Critical (defaults to Medium)")] string? priority = null,
|
||||
[Description("User ID to assign the issue to (optional)")] Guid? assigneeId = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Executing create_issue tool (SDK)");
|
||||
|
||||
// 1. Validate input
|
||||
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");
|
||||
|
||||
// Parse enums
|
||||
if (!Enum.TryParse<IssueType>(type, ignoreCase: true, out var issueType))
|
||||
throw new McpInvalidParamsException($"Invalid issue type: {type}. Must be Epic, Story, Task, or Bug");
|
||||
|
||||
var issuePriority = IssuePriority.Medium;
|
||||
if (!string.IsNullOrEmpty(priority))
|
||||
{
|
||||
if (!Enum.TryParse<IssuePriority>(priority, ignoreCase: true, out issuePriority))
|
||||
throw new McpInvalidParamsException($"Invalid priority: {priority}. Must be Low, Medium, High, or Critical");
|
||||
}
|
||||
|
||||
// 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 ?? string.Empty,
|
||||
type = issueType.ToString(),
|
||||
priority = issuePriority.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 = "create_issue",
|
||||
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 $"Issue creation request submitted for approval.\n\n" +
|
||||
$"**Pending Change ID**: {pendingChange.Id}\n" +
|
||||
$"**Status**: Pending Approval\n" +
|
||||
$"**Issue Type**: {issueType}\n" +
|
||||
$"**Title**: {title}\n" +
|
||||
$"**Priority**: {issuePriority}\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.";
|
||||
}
|
||||
catch (McpException)
|
||||
{
|
||||
throw; // Re-throw MCP exceptions as-is
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error executing create_issue tool (SDK)");
|
||||
throw new McpInvalidParamsException($"Error creating issue: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using System.ComponentModel;
|
||||
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.IssueManagement.Domain.Enums;
|
||||
using ColaFlow.Modules.IssueManagement.Domain.Repositories;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace ColaFlow.Modules.Mcp.Application.SdkTools;
|
||||
|
||||
/// <summary>
|
||||
/// MCP Tool: update_status (SDK-based implementation)
|
||||
/// Updates the status of an existing Issue
|
||||
/// Generates a Diff Preview and creates a PendingChange for approval
|
||||
/// </summary>
|
||||
[McpServerToolType]
|
||||
public class UpdateStatusSdkTool
|
||||
{
|
||||
private readonly IPendingChangeService _pendingChangeService;
|
||||
private readonly IIssueRepository _issueRepository;
|
||||
private readonly DiffPreviewService _diffPreviewService;
|
||||
private readonly ILogger<UpdateStatusSdkTool> _logger;
|
||||
|
||||
public UpdateStatusSdkTool(
|
||||
IPendingChangeService pendingChangeService,
|
||||
IIssueRepository issueRepository,
|
||||
DiffPreviewService diffPreviewService,
|
||||
ILogger<UpdateStatusSdkTool> 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));
|
||||
}
|
||||
|
||||
[McpServerTool]
|
||||
[Description("Update the status of an existing issue. Supports workflow transitions (Backlog → Todo → InProgress → Done). Requires human approval before being applied.")]
|
||||
public async Task<string> UpdateStatusAsync(
|
||||
[Description("The ID of the issue to update")] Guid issueId,
|
||||
[Description("The new status: Backlog, Todo, InProgress, or Done")] string newStatus,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Executing update_status tool (SDK)");
|
||||
|
||||
// 1. Validate and parse status
|
||||
if (!Enum.TryParse<IssueStatus>(newStatus, ignoreCase: true, out var statusEnum))
|
||||
throw new McpInvalidParamsException($"Invalid status: {newStatus}. Must be Backlog, Todo, InProgress, or Done");
|
||||
|
||||
// 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 = statusEnum.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)}"
|
||||
);
|
||||
|
||||
// 5. Create PendingChange
|
||||
var pendingChange = await _pendingChangeService.CreateAsync(
|
||||
new CreatePendingChangeRequest
|
||||
{
|
||||
ToolName = "update_status",
|
||||
Diff = diff,
|
||||
ExpirationHours = 24
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
_logger.LogInformation(
|
||||
"PendingChange created: {PendingChangeId} - UPDATE Issue {IssueId} status: {OldStatus} → {NewStatus}",
|
||||
pendingChange.Id, issueId, oldStatus, statusEnum);
|
||||
|
||||
// 6. Return pendingChangeId to AI
|
||||
return $"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**: {statusEnum}\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.";
|
||||
}
|
||||
catch (McpException)
|
||||
{
|
||||
throw; // Re-throw MCP exceptions as-is
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error executing update_status tool (SDK)");
|
||||
throw new McpInvalidParamsException($"Error updating issue status: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.1" />
|
||||
<PackageReference Include="ModelContextProtocol" Version="0.4.0-preview.3" />
|
||||
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="0.4.0-preview.3" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
Reference in New Issue
Block a user