Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .mcp/server.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{
"registry_name": "nuget",
"name": "Microsoft.DataFactory.MCP",
"version": "0.2.0-beta",
"version": "#{VERSION}#",
"package_arguments": [],
"environment_variables": []
}
Expand All @@ -16,6 +16,6 @@
"source": "github"
},
"version_detail": {
"version": "0.2.0-beta"
"version": "#{VERSION}#"
}
}
2 changes: 1 addition & 1 deletion DataFactory.MCP.Tests/DataFactory.MCP.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.6.25358.103" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.0-preview.6.25358.103" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.76.0" />
<PackageReference Include="ModelContextProtocol" Version="0.3.0-preview.2" />
<PackageReference Include="ModelContextProtocol" Version="0.3.0-preview.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0-preview.6.25358.103" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="10.0.0-preview.6.25358.103" />
<PackageReference Include="Xunit.SkippableFact" Version="1.5.23" />
Expand Down
2 changes: 2 additions & 0 deletions DataFactory.MCP.Tests/Infrastructure/McpTestFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,13 @@ public McpTestFixture()
services.AddScoped<IAuthenticationService, AuthenticationService>();
services.AddScoped<IFabricGatewayService, FabricGatewayService>();
services.AddScoped<IFabricConnectionService, FabricConnectionService>();
services.AddScoped<IFabricWorkspaceService, FabricWorkspaceService>();

// Register tools
services.AddScoped<AuthenticationTool>();
services.AddScoped<GatewayTool>();
services.AddScoped<ConnectionsTool>();
services.AddScoped<WorkspacesTool>();
})
.Build();

Expand Down
201 changes: 201 additions & 0 deletions DataFactory.MCP.Tests/Integration/WorkspacesToolIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
using Xunit;
using DataFactory.MCP.Tools;
using DataFactory.MCP.Tests.Infrastructure;
using DataFactory.MCP.Models;
using System.Text.Json;

namespace DataFactory.MCP.Tests.Integration;

/// <summary>
/// Integration tests for WorkspacesTool that call the actual MCP tool methods
/// without mocking to verify real behavior
/// </summary>
public class WorkspacesToolIntegrationTests : FabricToolIntegrationTestBase
{
private readonly WorkspacesTool _workspacesTool;

public WorkspacesToolIntegrationTests(McpTestFixture fixture) : base(fixture)
{
_workspacesTool = Fixture.GetService<WorkspacesTool>();
}

[Fact]
public async Task ListWorkspacesAsync_WithoutAuthentication_ShouldReturnAuthenticationError()
{
// Act
var result = await _workspacesTool.ListWorkspacesAsync();

// Assert
AssertAuthenticationError(result);
}

[Fact]
public async Task ListWorkspacesAsync_WithRoles_WithoutAuthentication_ShouldReturnAuthenticationError()
{
// Arrange
var testRoles = "Admin,Member";

// Act
var result = await _workspacesTool.ListWorkspacesAsync(testRoles);

// Assert
AssertAuthenticationError(result);
}

[Fact]
public async Task ListWorkspacesAsync_WithContinuationToken_WithoutAuthentication_ShouldReturnAuthenticationError()
{
// Arrange
var testToken = "test-continuation-token";

// Act
var result = await _workspacesTool.ListWorkspacesAsync(continuationToken: testToken);

// Assert
AssertAuthenticationError(result);
}

[Fact]
public async Task ListWorkspacesAsync_WithPreferWorkspaceSpecificEndpoints_WithoutAuthentication_ShouldReturnAuthenticationError()
{
// Act
var result = await _workspacesTool.ListWorkspacesAsync(preferWorkspaceSpecificEndpoints: true);

// Assert
AssertAuthenticationError(result);
}

[Fact]
public void WorkspacesTool_ShouldBeRegisteredInDI()
{
// Assert
Assert.NotNull(_workspacesTool);
Assert.IsType<WorkspacesTool>(_workspacesTool);
}

#region Authenticated Scenarios

/// <summary>
/// Test listing workspaces with authentication
/// </summary>
[SkippableFact]
public async Task ListWorkspacesAsync_WithAuthentication_ShouldReturnJsonResponseOrNoWorkspacesMessage()
{
// Arrange - Try to authenticate
var isAuthenticated = await TryAuthenticateAsync();

Skip.IfNot(isAuthenticated, "Skipping authenticated test - no valid credentials available");

// Act
var result = await _workspacesTool.ListWorkspacesAsync();

// Assert
AssertResult("workspaces", result);
}

[SkippableFact]
public async Task ListWorkspacesAsync_WithAuthentication_AndRoles_ShouldReturnJsonResponseOrNoWorkspacesMessage()
{
// Arrange - Try to authenticate
var isAuthenticated = await TryAuthenticateAsync();

Skip.IfNot(isAuthenticated, "Skipping authenticated test - no valid credentials available");

var testRoles = "Admin,Member,Contributor,Viewer";

// Act
var result = await _workspacesTool.ListWorkspacesAsync(testRoles);

// Assert
AssertResult("workspaces", result);

// Additional check for roles filtering
if (IsValidJson(result) && !result.Contains("No workspaces found"))
{
var jsonDoc = JsonDocument.Parse(result);
Assert.True(jsonDoc.RootElement.TryGetProperty("filteredByRoles", out var filteredByRoles));
Assert.True(filteredByRoles.GetBoolean());
Assert.True(jsonDoc.RootElement.TryGetProperty("roles", out var roles));
Assert.Equal(testRoles, roles.GetString());
}
}

[SkippableFact]
public async Task ListWorkspacesAsync_WithAuthentication_AndContinuationToken_ShouldHandleTokenParameter()
{
// Arrange - Try to authenticate
var isAuthenticated = await TryAuthenticateAsync();

Skip.IfNot(isAuthenticated, "Skipping authenticated test - no valid credentials available");

var testToken = "test-continuation-token-12345";

// Act
var result = await _workspacesTool.ListWorkspacesAsync(continuationToken: testToken);

// Assert
AssertResult("workspaces", result);
}

[SkippableFact]
public async Task ListWorkspacesAsync_WithAuthentication_AndPreferWorkspaceSpecificEndpoints_ShouldIncludeApiEndpoints()
{
// Arrange - Try to authenticate
var isAuthenticated = await TryAuthenticateAsync();

Skip.IfNot(isAuthenticated, "Skipping authenticated test - no valid credentials available");

// Act
var result = await _workspacesTool.ListWorkspacesAsync(preferWorkspaceSpecificEndpoints: true);

// Assert
AssertResult("workspaces", result);

// Additional check for API endpoints flag
if (IsValidJson(result) && !result.Contains("No workspaces found"))
{
var jsonDoc = JsonDocument.Parse(result);
Assert.True(jsonDoc.RootElement.TryGetProperty("includesApiEndpoints", out var includesApiEndpoints));
Assert.True(includesApiEndpoints.GetBoolean());
}
}

[SkippableFact]
public async Task ListWorkspacesAsync_WithAuthentication_AllParameters_ShouldHandleComplexRequest()
{
// Arrange - Try to authenticate
var isAuthenticated = await TryAuthenticateAsync();

Skip.IfNot(isAuthenticated, "Skipping authenticated test - no valid credentials available");

var testRoles = "Admin,Contributor";
var testToken = "test-token-complex";

// Act
var result = await _workspacesTool.ListWorkspacesAsync(
roles: testRoles,
continuationToken: testToken,
preferWorkspaceSpecificEndpoints: true);

// Assert
AssertResult("workspaces", result);

// Additional checks for all parameters
if (IsValidJson(result) && !result.Contains("No workspaces found"))
{
var jsonDoc = JsonDocument.Parse(result);

// Check roles filtering
Assert.True(jsonDoc.RootElement.TryGetProperty("filteredByRoles", out var filteredByRoles));
Assert.True(filteredByRoles.GetBoolean());
Assert.True(jsonDoc.RootElement.TryGetProperty("roles", out var roles));
Assert.Equal(testRoles, roles.GetString());

// Check API endpoints inclusion
Assert.True(jsonDoc.RootElement.TryGetProperty("includesApiEndpoints", out var includesApiEndpoints));
Assert.True(includesApiEndpoints.GetBoolean());
}
}

#endregion
}
10 changes: 7 additions & 3 deletions DataFactory.MCP/Abstractions/FabricServiceBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace DataFactory.MCP.Abstractions;
/// <summary>
/// Abstract base class for Microsoft Fabric API services providing common functionality
/// </summary>
public abstract class FabricServiceBase
public abstract class FabricServiceBase : IDisposable
{
protected const string BaseUrl = "https://api.fabric.microsoft.com/v1";
protected readonly HttpClient HttpClient;
Expand All @@ -19,11 +19,10 @@ public abstract class FabricServiceBase
protected readonly JsonSerializerOptions JsonOptions;

protected FabricServiceBase(
HttpClient httpClient,
ILogger logger,
IAuthenticationService authService)
{
HttpClient = httpClient;
HttpClient = new HttpClient();
Logger = logger;
AuthService = authService;

Expand Down Expand Up @@ -88,4 +87,9 @@ protected async Task EnsureAuthenticationAsync()
throw new HttpRequestException($"API request failed: {response.StatusCode} - {errorContent}");
}
}

public void Dispose()
{
HttpClient?.Dispose();
}
}
21 changes: 21 additions & 0 deletions DataFactory.MCP/Abstractions/Interfaces/IFabricWorkspaceService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using DataFactory.MCP.Models.Workspace;

namespace DataFactory.MCP.Abstractions.Interfaces;

/// <summary>
/// Service for interacting with Microsoft Fabric Workspaces API
/// </summary>
public interface IFabricWorkspaceService
{
/// <summary>
/// Lists all workspaces the user has permission for
/// </summary>
/// <param name="roles">A list of roles. Separate values using a comma. If not provided, all workspaces are returned.</param>
/// <param name="continuationToken">A token for retrieving the next page of results</param>
/// <param name="preferWorkspaceSpecificEndpoints">A setting that controls whether to include the workspace-specific API endpoint per workspace</param>
/// <returns>List of workspaces</returns>
Task<ListWorkspacesResponse> ListWorkspacesAsync(
string? roles = null,
string? continuationToken = null,
bool? preferWorkspaceSpecificEndpoints = null);
}
8 changes: 4 additions & 4 deletions DataFactory.MCP/DataFactory.MCP.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<RollForward>Major</RollForward>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
Expand Down Expand Up @@ -30,9 +30,9 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0-preview.6.25358.103" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.8" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.76.0" />
<PackageReference Include="ModelContextProtocol" Version="0.3.0-preview.2" />
<!-- tried 4.76 before broke macOS interactive auth -->
<PackageReference Include="Microsoft.Identity.Client" Version="4.67.0" />
<PackageReference Include="ModelContextProtocol" Version="0.3.0-preview.3" />
</ItemGroup>

</Project>
31 changes: 31 additions & 0 deletions DataFactory.MCP/Extensions/WorkspaceExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using DataFactory.MCP.Models.Workspace;

namespace DataFactory.MCP.Extensions;

/// <summary>
/// Extension methods for Workspace model transformations.
/// </summary>
public static class WorkspaceExtensions
{
/// <summary>
/// Formats a Workspace object for MCP API responses.
/// Provides consistent output format and handles optional properties appropriately.
/// </summary>
/// <param name="workspace">The workspace object to format</param>
/// <returns>Formatted object ready for JSON serialization</returns>
public static object ToFormattedInfo(this Workspace workspace)
{
var formattedInfo = new
{
Id = workspace.Id,
DisplayName = workspace.DisplayName,
Description = workspace.Description,
Type = workspace.Type.ToString(),
CapacityId = workspace.CapacityId,
DomainId = workspace.DomainId,
ApiEndpoint = workspace.ApiEndpoint
};

return formattedInfo;
}
}
2 changes: 1 addition & 1 deletion DataFactory.MCP/Models/AzureAdConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ public static class AzureAdConfiguration
/// <summary>
/// Redirect URI for interactive authentication
/// </summary>
public const string RedirectUri = "http://localhost:8080";
public const string RedirectUri = "http://localhost:0";
}
10 changes: 10 additions & 0 deletions DataFactory.MCP/Models/Messages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ public static class Messages
/// Error retrieving gateway message template
/// </summary>
public const string ErrorRetrievingGatewayTemplate = "Error retrieving gateway: {0}";

/// <summary>
/// Error listing workspaces message template
/// </summary>
public const string ErrorListingWorkspacesTemplate = "Error listing workspaces: {0}";
#endregion

#region Validation Messages
Expand Down Expand Up @@ -168,6 +173,11 @@ public static class Messages
/// Template for gateway not found message
/// </summary>
public const string GatewayNotFoundTemplate = "Gateway with ID '{0}' not found or you don't have permission to access it.";

/// <summary>
/// Message when no workspaces are found
/// </summary>
public const string NoWorkspacesFound = "No workspaces found. Make sure you have the required permissions (Workspace.Read.All or Workspace.ReadWrite.All).";
#endregion

#region Service Messages
Expand Down
Loading
Loading