Implemented 6 core MCP Resources for read-only AI agent access to ColaFlow data:
- projects.list - List all projects in current tenant
- projects.get/{id} - Get project details with full hierarchy
- issues.search - Search issues (Epics, Stories, Tasks) with filters
- issues.get/{id} - Get issue details (Epic/Story/Task)
- sprints.current - Get currently active Sprint(s)
- users.list - List team members in current tenant
Changes:
- Created IMcpResource interface and related DTOs (McpResourceRequest, McpResourceContent, McpResourceDescriptor)
- Implemented IMcpResourceRegistry and McpResourceRegistry for resource discovery and routing
- Created ResourcesReadMethodHandler for handling resources/read MCP method
- Updated ResourcesListMethodHandler to return actual resource catalog
- Implemented 6 concrete resource classes with multi-tenant isolation
- Registered all resources and handlers in McpServiceExtensions
- Added module references (ProjectManagement, Identity, IssueManagement domains)
- Updated package versions to 9.0.1 for consistency
- Created comprehensive unit tests (188 tests passing)
- Tests cover resource registry, URI matching, resource content generation
Technical Details:
- Multi-tenant isolation using TenantContext.GetCurrentTenantId()
- Resource URI routing supports templates (e.g., {id} parameters)
- Uses read-only repository queries (AsNoTracking) for performance
- JSON serialization with System.Text.Json
- Proper error handling with McpNotFoundException, McpInvalidParamsException
- Supports query parameters for filtering and pagination
- Auto-registration of resources at startup
Test Coverage:
- Resource registry tests (URI matching, registration, descriptors)
- Resource content generation tests
- Multi-tenant isolation verification
- All 188 tests passing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
153 lines
5.0 KiB
C#
153 lines
5.0 KiB
C#
using System.Text.Json;
|
|
using ColaFlow.Modules.Mcp.Application.Resources;
|
|
using ColaFlow.Modules.Mcp.Contracts.Resources;
|
|
using ColaFlow.Modules.ProjectManagement.Application.Common.Interfaces;
|
|
using ColaFlow.Modules.ProjectManagement.Domain.Aggregates.ProjectAggregate;
|
|
using ColaFlow.Modules.ProjectManagement.Domain.Enums;
|
|
using ColaFlow.Modules.ProjectManagement.Domain.Repositories;
|
|
using ColaFlow.Modules.ProjectManagement.Domain.ValueObjects;
|
|
using FluentAssertions;
|
|
using Microsoft.Extensions.Logging;
|
|
using NSubstitute;
|
|
|
|
namespace ColaFlow.Modules.Mcp.Tests.Resources;
|
|
|
|
/// <summary>
|
|
/// Unit tests for ProjectsListResource
|
|
/// </summary>
|
|
public class ProjectsListResourceTests
|
|
{
|
|
private readonly IProjectRepository _projectRepository;
|
|
private readonly ITenantContext _tenantContext;
|
|
private readonly ILogger<ProjectsListResource> _logger;
|
|
private readonly ProjectsListResource _sut;
|
|
private readonly Guid _tenantId = Guid.NewGuid();
|
|
|
|
public ProjectsListResourceTests()
|
|
{
|
|
_projectRepository = Substitute.For<IProjectRepository>();
|
|
_tenantContext = Substitute.For<ITenantContext>();
|
|
_logger = Substitute.For<ILogger<ProjectsListResource>>();
|
|
|
|
_tenantContext.GetCurrentTenantId().Returns(_tenantId);
|
|
|
|
_sut = new ProjectsListResource(_projectRepository, _tenantContext, _logger);
|
|
}
|
|
|
|
[Fact]
|
|
public void Uri_ReturnsCorrectValue()
|
|
{
|
|
// Assert
|
|
_sut.Uri.Should().Be("colaflow://projects.list");
|
|
}
|
|
|
|
[Fact]
|
|
public void Name_ReturnsCorrectValue()
|
|
{
|
|
// Assert
|
|
_sut.Name.Should().Be("Projects List");
|
|
}
|
|
|
|
[Fact]
|
|
public void MimeType_ReturnsApplicationJson()
|
|
{
|
|
// Assert
|
|
_sut.MimeType.Should().Be("application/json");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetContentAsync_WithNoProjects_ReturnsEmptyList()
|
|
{
|
|
// Arrange
|
|
_projectRepository.GetAllProjectsReadOnlyAsync(Arg.Any<CancellationToken>())
|
|
.Returns(new List<Project>());
|
|
|
|
var request = new McpResourceRequest { Uri = "colaflow://projects.list" };
|
|
|
|
// Act
|
|
var result = await _sut.GetContentAsync(request, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result.Uri.Should().Be("colaflow://projects.list");
|
|
result.MimeType.Should().Be("application/json");
|
|
|
|
var data = JsonSerializer.Deserialize<JsonElement>(result.Text);
|
|
data.GetProperty("projects").GetArrayLength().Should().Be(0);
|
|
data.GetProperty("total").GetInt32().Should().Be(0);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetContentAsync_WithProjects_ReturnsProjectsList()
|
|
{
|
|
// Arrange
|
|
var project1 = Project.Create(
|
|
ColaFlow.Modules.ProjectManagement.Domain.ValueObjects.TenantId.From(_tenantId),
|
|
"Project Alpha",
|
|
"First project",
|
|
"ALPHA",
|
|
UserId.Create());
|
|
|
|
var project2 = Project.Create(
|
|
ColaFlow.Modules.ProjectManagement.Domain.ValueObjects.TenantId.From(_tenantId),
|
|
"Project Beta",
|
|
"Second project",
|
|
"BETA",
|
|
UserId.Create());
|
|
|
|
_projectRepository.GetAllProjectsReadOnlyAsync(Arg.Any<CancellationToken>())
|
|
.Returns(new List<Project> { project1, project2 });
|
|
|
|
var request = new McpResourceRequest { Uri = "colaflow://projects.list" };
|
|
|
|
// Act
|
|
var result = await _sut.GetContentAsync(request, CancellationToken.None);
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
var data = JsonSerializer.Deserialize<JsonElement>(result.Text);
|
|
|
|
data.GetProperty("total").GetInt32().Should().Be(2);
|
|
|
|
var projects = data.GetProperty("projects");
|
|
projects.GetArrayLength().Should().Be(2);
|
|
|
|
var firstProject = projects[0];
|
|
firstProject.GetProperty("name").GetString().Should().Be("Project Alpha");
|
|
firstProject.GetProperty("key").GetString().Should().Be("ALPHA");
|
|
firstProject.GetProperty("status").GetString().Should().Be(ProjectStatus.Active.ToString());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetContentAsync_CallsTenantContext()
|
|
{
|
|
// Arrange
|
|
_projectRepository.GetAllProjectsReadOnlyAsync(Arg.Any<CancellationToken>())
|
|
.Returns(new List<Project>());
|
|
|
|
var request = new McpResourceRequest { Uri = "colaflow://projects.list" };
|
|
|
|
// Act
|
|
await _sut.GetContentAsync(request, CancellationToken.None);
|
|
|
|
// Assert
|
|
_tenantContext.Received(1).GetCurrentTenantId();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task GetContentAsync_CallsRepository()
|
|
{
|
|
// Arrange
|
|
_projectRepository.GetAllProjectsReadOnlyAsync(Arg.Any<CancellationToken>())
|
|
.Returns(new List<Project>());
|
|
|
|
var request = new McpResourceRequest { Uri = "colaflow://projects.list" };
|
|
|
|
// Act
|
|
await _sut.GetContentAsync(request, CancellationToken.None);
|
|
|
|
// Assert
|
|
await _projectRepository.Received(1).GetAllProjectsReadOnlyAsync(Arg.Any<CancellationToken>());
|
|
}
|
|
}
|