diff --git a/.mcp/server.json b/.mcp/server.json index b3f444c..b13bdf0 100644 --- a/.mcp/server.json +++ b/.mcp/server.json @@ -6,7 +6,7 @@ { "registry_name": "nuget", "name": "Microsoft.DataFactory.MCP", - "version": "0.2.0-beta", + "version": "#{VERSION}#", "package_arguments": [], "environment_variables": [] } @@ -16,6 +16,6 @@ "source": "github" }, "version_detail": { - "version": "0.2.0-beta" + "version": "#{VERSION}#" } } \ No newline at end of file diff --git a/DataFactory.MCP.Tests/DataFactory.MCP.Tests.csproj b/DataFactory.MCP.Tests/DataFactory.MCP.Tests.csproj index 422df9f..f9aecef 100644 --- a/DataFactory.MCP.Tests/DataFactory.MCP.Tests.csproj +++ b/DataFactory.MCP.Tests/DataFactory.MCP.Tests.csproj @@ -22,7 +22,7 @@ - + diff --git a/DataFactory.MCP.Tests/Infrastructure/McpTestFixture.cs b/DataFactory.MCP.Tests/Infrastructure/McpTestFixture.cs index ed43643..57136e4 100644 --- a/DataFactory.MCP.Tests/Infrastructure/McpTestFixture.cs +++ b/DataFactory.MCP.Tests/Infrastructure/McpTestFixture.cs @@ -51,11 +51,13 @@ public McpTestFixture() services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); // Register tools services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); }) .Build(); diff --git a/DataFactory.MCP.Tests/Integration/WorkspacesToolIntegrationTests.cs b/DataFactory.MCP.Tests/Integration/WorkspacesToolIntegrationTests.cs new file mode 100644 index 0000000..1a2f4ab --- /dev/null +++ b/DataFactory.MCP.Tests/Integration/WorkspacesToolIntegrationTests.cs @@ -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; + +/// +/// Integration tests for WorkspacesTool that call the actual MCP tool methods +/// without mocking to verify real behavior +/// +public class WorkspacesToolIntegrationTests : FabricToolIntegrationTestBase +{ + private readonly WorkspacesTool _workspacesTool; + + public WorkspacesToolIntegrationTests(McpTestFixture fixture) : base(fixture) + { + _workspacesTool = Fixture.GetService(); + } + + [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); + } + + #region Authenticated Scenarios + + /// + /// Test listing workspaces with authentication + /// + [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 +} \ No newline at end of file diff --git a/DataFactory.MCP/Abstractions/FabricServiceBase.cs b/DataFactory.MCP/Abstractions/FabricServiceBase.cs index 42d3d19..03f827b 100644 --- a/DataFactory.MCP/Abstractions/FabricServiceBase.cs +++ b/DataFactory.MCP/Abstractions/FabricServiceBase.cs @@ -10,7 +10,7 @@ namespace DataFactory.MCP.Abstractions; /// /// Abstract base class for Microsoft Fabric API services providing common functionality /// -public abstract class FabricServiceBase +public abstract class FabricServiceBase : IDisposable { protected const string BaseUrl = "https://api.fabric.microsoft.com/v1"; protected readonly HttpClient HttpClient; @@ -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; @@ -88,4 +87,9 @@ protected async Task EnsureAuthenticationAsync() throw new HttpRequestException($"API request failed: {response.StatusCode} - {errorContent}"); } } + + public void Dispose() + { + HttpClient?.Dispose(); + } } diff --git a/DataFactory.MCP/Abstractions/Interfaces/IFabricWorkspaceService.cs b/DataFactory.MCP/Abstractions/Interfaces/IFabricWorkspaceService.cs new file mode 100644 index 0000000..64a56c1 --- /dev/null +++ b/DataFactory.MCP/Abstractions/Interfaces/IFabricWorkspaceService.cs @@ -0,0 +1,21 @@ +using DataFactory.MCP.Models.Workspace; + +namespace DataFactory.MCP.Abstractions.Interfaces; + +/// +/// Service for interacting with Microsoft Fabric Workspaces API +/// +public interface IFabricWorkspaceService +{ + /// + /// Lists all workspaces the user has permission for + /// + /// A list of roles. Separate values using a comma. If not provided, all workspaces are returned. + /// A token for retrieving the next page of results + /// A setting that controls whether to include the workspace-specific API endpoint per workspace + /// List of workspaces + Task ListWorkspacesAsync( + string? roles = null, + string? continuationToken = null, + bool? preferWorkspaceSpecificEndpoints = null); +} \ No newline at end of file diff --git a/DataFactory.MCP/DataFactory.MCP.csproj b/DataFactory.MCP/DataFactory.MCP.csproj index 957d60c..bd603e4 100644 --- a/DataFactory.MCP/DataFactory.MCP.csproj +++ b/DataFactory.MCP/DataFactory.MCP.csproj @@ -1,7 +1,7 @@ - net10.0 + net9.0 Major Exe enable @@ -30,9 +30,9 @@ - - - + + + diff --git a/DataFactory.MCP/Extensions/WorkspaceExtensions.cs b/DataFactory.MCP/Extensions/WorkspaceExtensions.cs new file mode 100644 index 0000000..bfa0301 --- /dev/null +++ b/DataFactory.MCP/Extensions/WorkspaceExtensions.cs @@ -0,0 +1,31 @@ +using DataFactory.MCP.Models.Workspace; + +namespace DataFactory.MCP.Extensions; + +/// +/// Extension methods for Workspace model transformations. +/// +public static class WorkspaceExtensions +{ + /// + /// Formats a Workspace object for MCP API responses. + /// Provides consistent output format and handles optional properties appropriately. + /// + /// The workspace object to format + /// Formatted object ready for JSON serialization + 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; + } +} \ No newline at end of file diff --git a/DataFactory.MCP/Models/AzureAdConfiguration.cs b/DataFactory.MCP/Models/AzureAdConfiguration.cs index 8b11660..4bf80c9 100644 --- a/DataFactory.MCP/Models/AzureAdConfiguration.cs +++ b/DataFactory.MCP/Models/AzureAdConfiguration.cs @@ -29,5 +29,5 @@ public static class AzureAdConfiguration /// /// Redirect URI for interactive authentication /// - public const string RedirectUri = "http://localhost:8080"; + public const string RedirectUri = "http://localhost:0"; } diff --git a/DataFactory.MCP/Models/Messages.cs b/DataFactory.MCP/Models/Messages.cs index 66a0469..c05b65c 100644 --- a/DataFactory.MCP/Models/Messages.cs +++ b/DataFactory.MCP/Models/Messages.cs @@ -124,6 +124,11 @@ public static class Messages /// Error retrieving gateway message template /// public const string ErrorRetrievingGatewayTemplate = "Error retrieving gateway: {0}"; + + /// + /// Error listing workspaces message template + /// + public const string ErrorListingWorkspacesTemplate = "Error listing workspaces: {0}"; #endregion #region Validation Messages @@ -168,6 +173,11 @@ public static class Messages /// Template for gateway not found message /// public const string GatewayNotFoundTemplate = "Gateway with ID '{0}' not found or you don't have permission to access it."; + + /// + /// Message when no workspaces are found + /// + 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 diff --git a/DataFactory.MCP/Models/Workspace/Workspace.cs b/DataFactory.MCP/Models/Workspace/Workspace.cs new file mode 100644 index 0000000..1f8700f --- /dev/null +++ b/DataFactory.MCP/Models/Workspace/Workspace.cs @@ -0,0 +1,53 @@ +using System.Text.Json.Serialization; + +namespace DataFactory.MCP.Models.Workspace; + +/// +/// A workspace object representing a Microsoft Fabric workspace +/// +public class Workspace +{ + /// + /// The workspace ID + /// + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + /// + /// The workspace display name + /// + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; + + /// + /// The workspace description + /// + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; + + /// + /// The workspace type + /// + [JsonPropertyName("type")] + public WorkspaceType Type { get; set; } + + /// + /// The ID of the capacity the workspace is assigned to + /// + [JsonPropertyName("capacityId")] + public string? CapacityId { get; set; } + + /// + /// The ID of the domain the workspace is assigned to + /// + [JsonPropertyName("domainId")] + public string? DomainId { get; set; } + + /// + /// HTTP URL that represents the API endpoint specific to the workspace. + /// This endpoint value is returned when the user enables preferWorkspaceSpecificEndpoints. + /// It allows for API access over private links. + /// + [JsonPropertyName("apiEndpoint")] + public string? ApiEndpoint { get; set; } +} \ No newline at end of file diff --git a/DataFactory.MCP/Models/Workspace/WorkspaceResponses.cs b/DataFactory.MCP/Models/Workspace/WorkspaceResponses.cs new file mode 100644 index 0000000..7909a48 --- /dev/null +++ b/DataFactory.MCP/Models/Workspace/WorkspaceResponses.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace DataFactory.MCP.Models.Workspace; + +/// +/// Response containing a list of workspaces with optional pagination +/// +public class ListWorkspacesResponse +{ + /// + /// A list of workspaces + /// + [JsonPropertyName("value")] + public List Value { get; set; } = new(); + + /// + /// The token for the next result set batch. If there are no more records, it's removed from the response. + /// + [JsonPropertyName("continuationToken")] + public string? ContinuationToken { get; set; } + + /// + /// The URI of the next result set batch. If there are no more records, it's removed from the response. + /// + [JsonPropertyName("continuationUri")] + public string? ContinuationUri { get; set; } +} \ No newline at end of file diff --git a/DataFactory.MCP/Models/Workspace/WorkspaceType.cs b/DataFactory.MCP/Models/Workspace/WorkspaceType.cs new file mode 100644 index 0000000..204bae8 --- /dev/null +++ b/DataFactory.MCP/Models/Workspace/WorkspaceType.cs @@ -0,0 +1,25 @@ +using System.Text.Json.Serialization; + +namespace DataFactory.MCP.Models.Workspace; + +/// +/// A workspace type. Additional workspace types may be added over time. +/// +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum WorkspaceType +{ + /// + /// My folder or My workspace used to manage user items. + /// + Personal, + + /// + /// Workspace used to manage the Fabric items. + /// + Workspace, + + /// + /// Admin monitoring workspace. Contains admin reports such as the audit report and the usage and adoption report. + /// + AdminWorkspace +} \ No newline at end of file diff --git a/DataFactory.MCP/Program.cs b/DataFactory.MCP/Program.cs index ad32b68..8c7ca65 100644 --- a/DataFactory.MCP/Program.cs +++ b/DataFactory.MCP/Program.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using DataFactory.MCP.Models; using DataFactory.MCP.Tools; using DataFactory.MCP.Abstractions.Interfaces; using DataFactory.MCP.Services; @@ -11,24 +10,17 @@ // Configure all logs to go to stderr (stdout is used for the MCP protocol messages). builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace); -// Add authentication services directly -builder.Services.AddSingleton(); -builder.Services.AddTransient(); - -// Add HTTP client and Fabric Gateway services -builder.Services.AddHttpClient(); -builder.Services.AddTransient(); - -// Add HTTP client and Fabric Connection services -builder.Services.AddHttpClient(); -builder.Services.AddTransient(); - // Add the MCP services: the transport to use (stdio) and the tools to register. builder.Services + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddSingleton() .AddMcpServer() .WithStdioServerTransport() .WithTools() .WithTools() - .WithTools(); + .WithTools() + .WithTools(); await builder.Build().RunAsync(); diff --git a/DataFactory.MCP/Services/AuthenticationService.cs b/DataFactory.MCP/Services/AuthenticationService.cs index 074aacf..93a93ff 100644 --- a/DataFactory.MCP/Services/AuthenticationService.cs +++ b/DataFactory.MCP/Services/AuthenticationService.cs @@ -26,10 +26,10 @@ private void InitializeClientApplications() { // Initialize public client for interactive authentication _publicClientApp = PublicClientApplicationBuilder - .Create(AzureAdConfiguration.ClientId) - .WithAuthority(AzureAdConfiguration.Authority) - .WithRedirectUri(AzureAdConfiguration.RedirectUri) - .Build(); + .Create(AzureAdConfiguration.ClientId) + .WithAuthority(new Uri(AzureAdConfiguration.Authority)) + .WithRedirectUri(AzureAdConfiguration.RedirectUri) + .Build(); _logger.LogInformation(Messages.AzureAdClientInitializedSuccessfully); } diff --git a/DataFactory.MCP/Services/FabricConnectionService.cs b/DataFactory.MCP/Services/FabricConnectionService.cs index 77495ca..bee1061 100644 --- a/DataFactory.MCP/Services/FabricConnectionService.cs +++ b/DataFactory.MCP/Services/FabricConnectionService.cs @@ -12,10 +12,9 @@ namespace DataFactory.MCP.Services; public class FabricConnectionService : FabricServiceBase, IFabricConnectionService { public FabricConnectionService( - HttpClient httpClient, ILogger logger, IAuthenticationService authService) - : base(httpClient, logger, authService) + : base(logger, authService) { // Add the custom connection converter to handle polymorphic deserialization JsonOptions.Converters.Add(new ConnectionJsonConverter()); diff --git a/DataFactory.MCP/Services/FabricGatewayService.cs b/DataFactory.MCP/Services/FabricGatewayService.cs index 607692d..c38b31d 100644 --- a/DataFactory.MCP/Services/FabricGatewayService.cs +++ b/DataFactory.MCP/Services/FabricGatewayService.cs @@ -12,10 +12,9 @@ namespace DataFactory.MCP.Services; public class FabricGatewayService : FabricServiceBase, IFabricGatewayService { public FabricGatewayService( - HttpClient httpClient, ILogger logger, IAuthenticationService authService) - : base(httpClient, logger, authService) + : base(logger, authService) { } diff --git a/DataFactory.MCP/Services/FabricWorkspaceService.cs b/DataFactory.MCP/Services/FabricWorkspaceService.cs new file mode 100644 index 0000000..dd86fcb --- /dev/null +++ b/DataFactory.MCP/Services/FabricWorkspaceService.cs @@ -0,0 +1,88 @@ +using DataFactory.MCP.Abstractions; +using DataFactory.MCP.Abstractions.Interfaces; +using DataFactory.MCP.Models.Workspace; +using Microsoft.Extensions.Logging; +using System.Text; + +namespace DataFactory.MCP.Services; + +/// +/// Service for interacting with Microsoft Fabric Workspaces API +/// +public class FabricWorkspaceService : FabricServiceBase, IFabricWorkspaceService +{ + public FabricWorkspaceService( + ILogger logger, + IAuthenticationService authService) + : base(logger, authService) + { + } + + public async Task ListWorkspacesAsync( + string? roles = null, + string? continuationToken = null, + bool? preferWorkspaceSpecificEndpoints = null) + { + try + { + await EnsureAuthenticationAsync(); + + var url = BuildWorkspacesUrl(roles, continuationToken, preferWorkspaceSpecificEndpoints); + + Logger.LogInformation("Fetching workspaces from: {Url}", url); + + var response = await HttpClient.GetAsync(url); + + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(); + var workspacesResponse = System.Text.Json.JsonSerializer.Deserialize(content, JsonOptions); + + Logger.LogInformation("Successfully retrieved {Count} workspaces", workspacesResponse?.Value?.Count ?? 0); + return workspacesResponse ?? new ListWorkspacesResponse(); + } + else + { + var errorContent = await response.Content.ReadAsStringAsync(); + Logger.LogError("API request failed. Status: {StatusCode}, Content: {Content}", + response.StatusCode, errorContent); + + throw new HttpRequestException($"API request failed: {response.StatusCode} - {errorContent}"); + } + } + catch (Exception ex) + { + Logger.LogError(ex, "Error fetching workspaces"); + throw; + } + } + + private static string BuildWorkspacesUrl(string? roles, string? continuationToken, bool? preferWorkspaceSpecificEndpoints) + { + var url = new StringBuilder($"{BaseUrl}/workspaces"); + var queryParams = new List(); + + if (!string.IsNullOrEmpty(roles)) + { + queryParams.Add($"roles={Uri.EscapeDataString(roles)}"); + } + + if (!string.IsNullOrEmpty(continuationToken)) + { + queryParams.Add($"continuationToken={Uri.EscapeDataString(continuationToken)}"); + } + + if (preferWorkspaceSpecificEndpoints.HasValue) + { + queryParams.Add($"preferWorkspaceSpecificEndpoints={preferWorkspaceSpecificEndpoints.Value.ToString().ToLower()}"); + } + + if (queryParams.Any()) + { + url.Append("?"); + url.Append(string.Join("&", queryParams)); + } + + return url.ToString(); + } +} \ No newline at end of file diff --git a/DataFactory.MCP/Tools/WorkspacesTool.cs b/DataFactory.MCP/Tools/WorkspacesTool.cs new file mode 100644 index 0000000..b2eb2f6 --- /dev/null +++ b/DataFactory.MCP/Tools/WorkspacesTool.cs @@ -0,0 +1,66 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; +using DataFactory.MCP.Abstractions.Interfaces; +using DataFactory.MCP.Extensions; +using DataFactory.MCP.Models; +using System.Text.Json; + +namespace DataFactory.MCP.Tools; + +[McpServerToolType] +public class WorkspacesTool +{ + private readonly IFabricWorkspaceService _workspaceService; + + public WorkspacesTool(IFabricWorkspaceService workspaceService) + { + _workspaceService = workspaceService; + } + + [McpServerTool, Description(@"Lists all workspaces the user has permission for. Returns workspaces filtered by the specified roles if provided.")] + public async Task ListWorkspacesAsync( + [Description("A list of roles. Separate values using a comma (e.g., 'Admin,Member,Contributor,Viewer'). If not provided, all workspaces are returned.")] string? roles = null, + [Description("A token for retrieving the next page of results (optional)")] string? continuationToken = null, + [Description("Include workspace-specific API endpoints in the response (true/false, optional)")] bool? preferWorkspaceSpecificEndpoints = null) + { + try + { + var response = await _workspaceService.ListWorkspacesAsync(roles, continuationToken, preferWorkspaceSpecificEndpoints); + + if (!response.Value.Any()) + { + return Messages.NoWorkspacesFound; + } + + var result = new + { + TotalCount = response.Value.Count, + ContinuationToken = response.ContinuationToken, + ContinuationUri = response.ContinuationUri, + HasMoreResults = !string.IsNullOrEmpty(response.ContinuationToken), + FilteredByRoles = !string.IsNullOrEmpty(roles), + Roles = roles, + IncludesApiEndpoints = preferWorkspaceSpecificEndpoints == true, + Workspaces = response.Value.Select(w => w.ToFormattedInfo()) + }; + + return JsonSerializer.Serialize(result, new JsonSerializerOptions + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }); + } + catch (UnauthorizedAccessException ex) + { + return string.Format(Messages.AuthenticationErrorTemplate, ex.Message); + } + catch (HttpRequestException ex) + { + return string.Format(Messages.ApiRequestFailedTemplate, ex.Message); + } + catch (Exception ex) + { + return string.Format(Messages.ErrorListingWorkspacesTemplate, ex.Message); + } + } +} \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 7cb1d90..43dad79 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ 0 - 3 + 4 0 beta diff --git a/README.md b/README.md index 1794f21..f05ff6c 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ A Model Context Protocol (MCP) server for Microsoft Data Factory and Azure Fabri - 🔐 **Azure AD Authentication**: Interactive and service principal authentication - 🌐 **Gateway Management**: List and manage Azure Data Factory gateways - 🔗 **Connection Management**: List and retrieve details for Azure Data Factory connections +- **Workspace Management**: List and manage Microsoft Fabric workspaces - 🏗️ **Microsoft Fabric Integration**: Support for on-premises, personal, and virtual network gateways - 📦 **NuGet Distribution**: Available as a NuGet package for easy integration - 🔧 **MCP Protocol**: Built using the official MCP C# SDK @@ -16,6 +17,7 @@ A Model Context Protocol (MCP) server for Microsoft Data Factory and Azure Fabri - **Authentication**: `authenticate_interactive`, `authenticate_service_principal`, `get_authentication_status`, `get_access_token`, `sign_out` - **Gateway Management**: `list_gateways`, `get_gateway` - **Connection Management**: `list_connections`, `get_connection` +- **Workspace Management**: `list_workspaces`, `list_workspaces_summary` ## Quick Start @@ -35,7 +37,7 @@ A Model Context Protocol (MCP) server for Microsoft Data Factory and Azure Fabri "args": [ "Microsoft.DataFactory.MCP", "--version", - "0.2.0-beta", + "#{VERSION}#", "--yes" ] } @@ -80,6 +82,7 @@ See the detailed guides for comprehensive usage instructions: - **Authentication**: See [Authentication Guide](https://github.com/microsoft/DataFactory.MCP/blob/main/docs/authentication.md) - **Gateway Management**: See [Gateway Management Guide](https://github.com/microsoft/DataFactory.MCP/blob/main/docs/gateway-management.md) - **Connection Management**: See [Connection Management Guide](https://github.com/microsoft/DataFactory.MCP/blob/main/docs/connection-management.md) +- **Workspace Management**: See [Workspace Management Guide](https://github.com/microsoft/DataFactory.MCP/blob/main/docs/workspace-management.md) ## Development @@ -110,6 +113,7 @@ For complete documentation, see our **[Documentation Index](https://github.com/m - **[Authentication Guide](https://github.com/microsoft/DataFactory.MCP/blob/main/docs/authentication.md)** - Complete authentication setup and usage - **[Gateway Management Guide](https://github.com/microsoft/DataFactory.MCP/blob/main/docs/gateway-management.md)** - Gateway operations and examples - **[Connection Management Guide](https://github.com/microsoft/DataFactory.MCP/blob/main/docs/connection-management.md)** - Connection operations and examples +- **[Workspace Management Guide](https://github.com/microsoft/DataFactory.MCP/blob/main/docs/workspace-management.md)** - Workspace operations and examples - **[Architecture Guide](https://github.com/microsoft/DataFactory.MCP/blob/main/docs/ARCHITECTURE.md)** - Technical architecture and design details ## Contributing diff --git a/Update-ServerVersion.ps1 b/Update-ServerVersion.ps1 index 4ac408f..5f457ce 100644 --- a/Update-ServerVersion.ps1 +++ b/Update-ServerVersion.ps1 @@ -1,4 +1,6 @@ # Update server.json and README.md with current version from Directory.Build.props +# This script replaces version placeholders (#{VERSION}#) and actual version numbers +# with the current version from Directory.Build.props [CmdletBinding()] param( [string]$Version, @@ -63,11 +65,13 @@ function Update-ReadmeVersion { $originalContent = $content # Simple regex to find and replace version in README - # Looking for version pattern after "--version", - $pattern = '("--version",\s*[\r\n]+\s*)"[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?"' + # Looking for version pattern after "--version", supporting both actual versions and placeholders + $pattern1 = '("--version",\s*[\r\n]+\s*)"[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9]+)?"' + $pattern2 = '("--version",\s*[\r\n]+\s*)"#{VERSION}#"' $replacement = "`$1`"$NewVersion`"" - $content = $content -replace $pattern, $replacement + $content = $content -replace $pattern1, $replacement + $content = $content -replace $pattern2, $replacement if ($content -ne $originalContent) { Set-Content $Path -Value $content -Encoding UTF8 -NoNewline diff --git a/docs/index.md b/docs/index.md index 401f542..3b84b34 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,6 +7,7 @@ Comprehensive documentation for the Microsoft Data Factory MCP Server. - **[Authentication Guide](authentication.md)** - Setup and configuration - **[Gateway Management Guide](gateway-management.md)** - Managing Azure Data Factory gateways - **[Connection Management Guide](connection-management.md)** - Managing Azure Data Factory connections +- **[Workspace Management Guide](workspace-management.md)** - Managing Microsoft Fabric workspaces ## Technical Documentation diff --git a/docs/workspace-management.md b/docs/workspace-management.md new file mode 100644 index 0000000..f99f97b --- /dev/null +++ b/docs/workspace-management.md @@ -0,0 +1,116 @@ +# Workspace Management Guide + +This guide covers how to use the Microsoft Data Factory MCP Server for managing Microsoft Fabric workspaces. + +## Overview + +The workspace management tools allow you to: +- List all accessible workspaces with filtering by role +- Work with different workspace types (Personal, Shared, Admin) +- Navigate paginated results for large workspace collections + +## Available Operations + +### List Workspaces + +Retrieve a list of all workspaces you have access to with full details. + +#### Usage +``` +list_workspaces +``` + +#### With Role Filtering +``` +list_workspaces(roles: "Admin,Member") +``` + +#### With Pagination +``` +list_workspaces(continuationToken: "next-page-token") +``` + +#### With Workspace-Specific Endpoints +``` +list_workspaces(preferWorkspaceSpecificEndpoints: true) +``` + +#### Response Format +```json +{ + "totalCount": 10, + "continuationToken": "eyJza2lwIjoyMCwidGFrZSI6MjB9", + "hasMoreResults": true, + "workspaces": [ + { + "id": "12345678-1234-1234-1234-123456789012", + "name": "Sales Analytics Workspace", + "description": "Workspace for sales team analytics and reporting", + "type": "PersonalGroup", + "state": "Active", + "isReadOnly": false, + "isOnDedicatedCapacity": true, + "capacityId": "87654321-4321-4321-4321-210987654321", + "defaultDatasetStorageFormat": "Small" + } + ] +} +``` + +## Role-Based Filtering + +The workspace tools support filtering by user roles within workspaces: + +- **Admin**: Full administrative access to the workspace +- **Member**: Can contribute content and manage workspace settings +- **Contributor**: Can contribute content but cannot manage workspace settings +- **Viewer**: Read-only access to workspace content + +### Multiple Role Filtering +``` +# Filter by multiple roles (comma-separated) +list_workspaces(roles: "Admin,Member,Contributor") + +# Filter by single role +list_workspaces(roles: "Admin") +``` + +## Workspace Types + +The system supports different workspace types: + +- **PersonalGroup**: Personal workspace for individual users +- **Group**: Shared workspace for team collaboration +- **AdminWorkspace**: Administrative workspace with elevated privileges + +## Usage Examples + +### Basic Workspace Operations +``` +# List all accessible workspaces +> show me all my fabric workspaces + +# List workspaces where I'm an admin +> list workspaces where I have admin role + +# List workspaces with pagination +> list my workspaces with continuation token abc123 +``` + +### Advanced Filtering +``` +# List workspaces by specific roles +> show me workspaces where I'm an admin or member + +# List only active workspaces +> show me all active workspaces +``` + +### Pagination Scenarios +``` +# Handle large workspace collections +> list all my workspaces (this may return a continuation token for more results) + +# Continue listing with pagination token +> continue listing workspaces with token eyJza2lwIjoyMCwidGFrZSI6MjB9 +``` \ No newline at end of file