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>
145 lines
4.3 KiB
C#
145 lines
4.3 KiB
C#
using ColaFlow.Modules.Mcp.Application.Services;
|
|
using ColaFlow.Modules.Mcp.Contracts.Resources;
|
|
using FluentAssertions;
|
|
using Microsoft.Extensions.Logging;
|
|
using NSubstitute;
|
|
|
|
namespace ColaFlow.Modules.Mcp.Tests.Services;
|
|
|
|
/// <summary>
|
|
/// Unit tests for McpResourceRegistry
|
|
/// </summary>
|
|
public class McpResourceRegistryTests
|
|
{
|
|
private readonly ILogger<McpResourceRegistry> _logger;
|
|
private readonly McpResourceRegistry _sut;
|
|
|
|
public McpResourceRegistryTests()
|
|
{
|
|
_logger = Substitute.For<ILogger<McpResourceRegistry>>();
|
|
_sut = new McpResourceRegistry(_logger);
|
|
}
|
|
|
|
[Fact]
|
|
public void RegisterResource_AddsResourceToRegistry()
|
|
{
|
|
// Arrange
|
|
var resource = CreateMockResource("colaflow://test", "Test Resource");
|
|
|
|
// Act
|
|
_sut.RegisterResource(resource);
|
|
|
|
// Assert
|
|
var resources = _sut.GetAllResources();
|
|
resources.Should().ContainSingle();
|
|
resources[0].Uri.Should().Be("colaflow://test");
|
|
}
|
|
|
|
[Fact]
|
|
public void RegisterResource_WithDuplicateUri_Overwrites()
|
|
{
|
|
// Arrange
|
|
var resource1 = CreateMockResource("colaflow://test", "Test Resource 1");
|
|
var resource2 = CreateMockResource("colaflow://test", "Test Resource 2");
|
|
|
|
// Act
|
|
_sut.RegisterResource(resource1);
|
|
_sut.RegisterResource(resource2);
|
|
|
|
// Assert
|
|
var resources = _sut.GetAllResources();
|
|
resources.Count.Should().Be(2); // Both are in the list
|
|
var foundResource = _sut.GetResourceByUri("colaflow://test");
|
|
foundResource!.Name.Should().Be("Test Resource 2"); // But only the last one is returned
|
|
}
|
|
|
|
[Fact]
|
|
public void GetResourceByUri_WithExactMatch_ReturnsResource()
|
|
{
|
|
// Arrange
|
|
var resource = CreateMockResource("colaflow://projects.list", "Projects List");
|
|
_sut.RegisterResource(resource);
|
|
|
|
// Act
|
|
var result = _sut.GetResourceByUri("colaflow://projects.list");
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result!.Uri.Should().Be("colaflow://projects.list");
|
|
}
|
|
|
|
[Fact]
|
|
public void GetResourceByUri_WithTemplateMatch_ReturnsResource()
|
|
{
|
|
// Arrange
|
|
var resource = CreateMockResource("colaflow://projects.get/{id}", "Project Details");
|
|
_sut.RegisterResource(resource);
|
|
|
|
// Act
|
|
var result = _sut.GetResourceByUri("colaflow://projects.get/123");
|
|
|
|
// Assert
|
|
result.Should().NotBeNull();
|
|
result!.Uri.Should().Be("colaflow://projects.get/{id}");
|
|
}
|
|
|
|
[Fact]
|
|
public void GetResourceByUri_WithNonExistentUri_ReturnsNull()
|
|
{
|
|
// Act
|
|
var result = _sut.GetResourceByUri("colaflow://nonexistent");
|
|
|
|
// Assert
|
|
result.Should().BeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void GetResourceDescriptors_ReturnsAllDescriptors()
|
|
{
|
|
// Arrange
|
|
var resource1 = CreateMockResource("colaflow://test1", "Test 1");
|
|
var resource2 = CreateMockResource("colaflow://test2", "Test 2");
|
|
_sut.RegisterResource(resource1);
|
|
_sut.RegisterResource(resource2);
|
|
|
|
// Act
|
|
var descriptors = _sut.GetResourceDescriptors();
|
|
|
|
// Assert
|
|
descriptors.Should().HaveCount(2);
|
|
descriptors.Should().Contain(d => d.Uri == "colaflow://test1");
|
|
descriptors.Should().Contain(d => d.Uri == "colaflow://test2");
|
|
}
|
|
|
|
[Fact]
|
|
public void GetAllResources_ReturnsReadOnlyList()
|
|
{
|
|
// Arrange
|
|
var resource = CreateMockResource("colaflow://test", "Test");
|
|
_sut.RegisterResource(resource);
|
|
|
|
// Act
|
|
var resources = _sut.GetAllResources();
|
|
|
|
// Assert
|
|
resources.Should().BeAssignableTo<IReadOnlyList<IMcpResource>>();
|
|
}
|
|
|
|
private IMcpResource CreateMockResource(string uri, string name)
|
|
{
|
|
var resource = Substitute.For<IMcpResource>();
|
|
resource.Uri.Returns(uri);
|
|
resource.Name.Returns(name);
|
|
resource.Description.Returns($"Description for {name}");
|
|
resource.MimeType.Returns("application/json");
|
|
resource.GetContentAsync(Arg.Any<McpResourceRequest>(), Arg.Any<CancellationToken>())
|
|
.Returns(new McpResourceContent
|
|
{
|
|
Uri = uri,
|
|
MimeType = "application/json",
|
|
Text = "{}"
|
|
});
|
|
return resource;
|
|
}
|
|
}
|