From c0ec8caea2967bd37509e8146aa9c35f2da33e93 Mon Sep 17 00:00:00 2001 From: ebramtawfik Date: Thu, 18 Sep 2025 12:02:13 -0700 Subject: [PATCH 01/11] Workspace --- .../Interfaces/IFabricWorkspaceService.cs | 21 ++++ .../Extensions/WorkspaceExtensions.cs | 49 ++++++++ DataFactory.MCP/Models/Messages.cs | 10 ++ DataFactory.MCP/Models/Workspace/Workspace.cs | 53 +++++++++ .../Models/Workspace/WorkspaceResponses.cs | 27 +++++ .../Models/Workspace/WorkspaceType.cs | 25 ++++ DataFactory.MCP/Program.cs | 8 +- .../Services/FabricWorkspaceService.cs | 89 ++++++++++++++ DataFactory.MCP/Tools/WorkspacesTool.cs | 110 ++++++++++++++++++ 9 files changed, 390 insertions(+), 2 deletions(-) create mode 100644 DataFactory.MCP/Abstractions/Interfaces/IFabricWorkspaceService.cs create mode 100644 DataFactory.MCP/Extensions/WorkspaceExtensions.cs create mode 100644 DataFactory.MCP/Models/Workspace/Workspace.cs create mode 100644 DataFactory.MCP/Models/Workspace/WorkspaceResponses.cs create mode 100644 DataFactory.MCP/Models/Workspace/WorkspaceType.cs create mode 100644 DataFactory.MCP/Services/FabricWorkspaceService.cs create mode 100644 DataFactory.MCP/Tools/WorkspacesTool.cs 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/Extensions/WorkspaceExtensions.cs b/DataFactory.MCP/Extensions/WorkspaceExtensions.cs new file mode 100644 index 0000000..46b7020 --- /dev/null +++ b/DataFactory.MCP/Extensions/WorkspaceExtensions.cs @@ -0,0 +1,49 @@ +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; + } + + /// + /// Formats a Workspace object for summary display (minimal information). + /// Used when displaying multiple workspaces in a list format. + /// + /// The workspace object to format + /// Formatted summary object + public static object ToSummaryInfo(this Workspace workspace) + { + return new + { + Id = workspace.Id, + DisplayName = workspace.DisplayName, + Type = workspace.Type.ToString(), + HasCapacity = !string.IsNullOrEmpty(workspace.CapacityId), + HasDescription = !string.IsNullOrEmpty(workspace.Description) + }; + } +} \ No newline at end of file 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..d467919 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; @@ -23,12 +22,17 @@ builder.Services.AddHttpClient(); builder.Services.AddTransient(); +// Add HTTP client and Fabric Workspace services +builder.Services.AddHttpClient(); +builder.Services.AddTransient(); + // Add the MCP services: the transport to use (stdio) and the tools to register. builder.Services .AddMcpServer() .WithStdioServerTransport() .WithTools() .WithTools() - .WithTools(); + .WithTools() + .WithTools(); await builder.Build().RunAsync(); diff --git a/DataFactory.MCP/Services/FabricWorkspaceService.cs b/DataFactory.MCP/Services/FabricWorkspaceService.cs new file mode 100644 index 0000000..bb4d9fa --- /dev/null +++ b/DataFactory.MCP/Services/FabricWorkspaceService.cs @@ -0,0 +1,89 @@ +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( + HttpClient httpClient, + ILogger logger, + IAuthenticationService authService) + : base(httpClient, 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..b6ee05e --- /dev/null +++ b/DataFactory.MCP/Tools/WorkspacesTool.cs @@ -0,0 +1,110 @@ +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); + } + } + + [McpServerTool, Description(@"Gets a summary list of workspaces with essential information only. Useful for quick overview.")] + public async Task ListWorkspacesSummaryAsync( + [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) + { + try + { + var response = await _workspaceService.ListWorkspacesAsync(roles, continuationToken, false); + + if (!response.Value.Any()) + { + return Messages.NoWorkspacesFound; + } + + var result = new + { + TotalCount = response.Value.Count, + ContinuationToken = response.ContinuationToken, + HasMoreResults = !string.IsNullOrEmpty(response.ContinuationToken), + FilteredByRoles = !string.IsNullOrEmpty(roles), + Roles = roles, + Workspaces = response.Value.Select(w => w.ToSummaryInfo()) + }; + + 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 From 6cd3b19b5e395f1192f1aff6da512240a70c4e50 Mon Sep 17 00:00:00 2001 From: ebramtawfik Date: Thu, 18 Sep 2025 12:11:24 -0700 Subject: [PATCH 02/11] Tests --- .../Infrastructure/McpTestFixture.cs | 2 + .../WorkspacesToolIntegrationTests.cs | 273 ++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 DataFactory.MCP.Tests/Integration/WorkspacesToolIntegrationTests.cs 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..0a3ad0d --- /dev/null +++ b/DataFactory.MCP.Tests/Integration/WorkspacesToolIntegrationTests.cs @@ -0,0 +1,273 @@ +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 async Task ListWorkspacesSummaryAsync_WithoutAuthentication_ShouldReturnAuthenticationError() + { + // Act + var result = await _workspacesTool.ListWorkspacesSummaryAsync(); + + // Assert + AssertAuthenticationError(result); + } + + [Fact] + public async Task ListWorkspacesSummaryAsync_WithRoles_WithoutAuthentication_ShouldReturnAuthenticationError() + { + // Arrange + var testRoles = "Viewer,Contributor"; + + // Act + var result = await _workspacesTool.ListWorkspacesSummaryAsync(testRoles); + + // 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 ListWorkspacesSummaryAsync_WithAuthentication_ShouldReturnSummaryFormat() + { + // Arrange - Try to authenticate + var isAuthenticated = await TryAuthenticateAsync(); + + Skip.IfNot(isAuthenticated, "Skipping authenticated test - no valid credentials available"); + + // Act + var result = await _workspacesTool.ListWorkspacesSummaryAsync(); + + // Assert + AssertResult("workspaces", result); + + // Additional check that summary format doesn't include API endpoints flag + if (IsValidJson(result) && !result.Contains("No workspaces found")) + { + var jsonDoc = JsonDocument.Parse(result); + Assert.False(jsonDoc.RootElement.TryGetProperty("includesApiEndpoints", out _)); + } + } + + [SkippableFact] + public async Task ListWorkspacesSummaryAsync_WithAuthentication_AndRoles_ShouldReturnFilteredSummary() + { + // Arrange - Try to authenticate + var isAuthenticated = await TryAuthenticateAsync(); + + Skip.IfNot(isAuthenticated, "Skipping authenticated test - no valid credentials available"); + + var testRoles = "Admin,Member"; + + // Act + var result = await _workspacesTool.ListWorkspacesSummaryAsync(testRoles); + + // Assert + AssertResult("workspaces", result); + + // Additional check for roles filtering in summary + 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_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 From e85679722c01aa7a5aecf3529d2fa67215ec3926 Mon Sep 17 00:00:00 2001 From: ebramtawfik Date: Thu, 18 Sep 2025 12:14:22 -0700 Subject: [PATCH 03/11] 0.4.0 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From ca90ea93ebcbeb1a3bfe1735c90793d201454386 Mon Sep 17 00:00:00 2001 From: ebramtawfik Date: Thu, 18 Sep 2025 12:19:14 -0700 Subject: [PATCH 04/11] version holder --- .mcp/server.json | 4 ++-- README.md | 2 +- Update-ServerVersion.ps1 | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) 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/README.md b/README.md index 1794f21..1301acc 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,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" ] } diff --git a/Update-ServerVersion.ps1 b/Update-ServerVersion.ps1 index 4ac408f..8c7c47e 100644 --- a/Update-ServerVersion.ps1 +++ b/Update-ServerVersion.ps1 @@ -63,11 +63,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 From 414d0b122b58f6fcee23e2cd92a2d5b5fdbed926 Mon Sep 17 00:00:00 2001 From: ebramtawfik Date: Thu, 18 Sep 2025 12:19:14 -0700 Subject: [PATCH 05/11] version holder --- .mcp/server.json | 4 ++-- README.md | 2 +- Update-ServerVersion.ps1 | 10 +++++++--- 3 files changed, 10 insertions(+), 6 deletions(-) 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/README.md b/README.md index 1794f21..1301acc 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,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" ] } 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 From 29cea2c674fa32f3a8994691cc99325b23ccd7b8 Mon Sep 17 00:00:00 2001 From: ebramtawfik Date: Thu, 18 Sep 2025 13:05:59 -0700 Subject: [PATCH 06/11] Readme --- README.md | 4 + docs/index.md | 1 + docs/workspace-management.md | 159 +++++++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+) create mode 100644 docs/workspace-management.md diff --git a/README.md b/README.md index 1301acc..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 @@ -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/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..6c21137 --- /dev/null +++ b/docs/workspace-management.md @@ -0,0 +1,159 @@ +# 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 +- Retrieve summary information about workspaces +- 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" + } + ] +} +``` + +### List Workspaces Summary + +Retrieve a summary list of workspaces with essential information only for quick overview. + +#### Usage +``` +list_workspaces_summary +``` + +#### With Role Filtering +``` +list_workspaces_summary(roles: "Admin,Contributor") +``` + +#### With Pagination +``` +list_workspaces_summary(continuationToken: "next-page-token") +``` + +#### Response Format +```json +{ + "totalCount": 10, + "continuationToken": "eyJza2lwIjoyMCwidGFrZSI6MjB9", + "hasMoreResults": true, + "workspaces": [ + { + "id": "12345678-1234-1234-1234-123456789012", + "name": "Sales Analytics Workspace", + "type": "PersonalGroup", + "state": "Active" + } + ] +} +``` + +## 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_summary(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 + +# Get a summary of all workspaces +> give me a summary of my workspaces + +# 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 + +# Get workspace summary for contributors +> give me a summary of workspaces where I'm a contributor +``` + +### 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 From 164eeaa1bfa8b3638a827e6664b779df646cee96 Mon Sep 17 00:00:00 2001 From: ebramtawfik Date: Thu, 18 Sep 2025 13:31:27 -0700 Subject: [PATCH 07/11] removing ListWorkspacesSummaryAsync --- .../WorkspacesToolIntegrationTests.cs | 72 ------------------- .../Extensions/WorkspaceExtensions.cs | 18 ----- DataFactory.MCP/Tools/WorkspacesTool.cs | 44 ------------ docs/workspace-management.md | 45 +----------- 4 files changed, 1 insertion(+), 178 deletions(-) diff --git a/DataFactory.MCP.Tests/Integration/WorkspacesToolIntegrationTests.cs b/DataFactory.MCP.Tests/Integration/WorkspacesToolIntegrationTests.cs index 0a3ad0d..1a2f4ab 100644 --- a/DataFactory.MCP.Tests/Integration/WorkspacesToolIntegrationTests.cs +++ b/DataFactory.MCP.Tests/Integration/WorkspacesToolIntegrationTests.cs @@ -65,29 +65,6 @@ public async Task ListWorkspacesAsync_WithPreferWorkspaceSpecificEndpoints_Witho AssertAuthenticationError(result); } - [Fact] - public async Task ListWorkspacesSummaryAsync_WithoutAuthentication_ShouldReturnAuthenticationError() - { - // Act - var result = await _workspacesTool.ListWorkspacesSummaryAsync(); - - // Assert - AssertAuthenticationError(result); - } - - [Fact] - public async Task ListWorkspacesSummaryAsync_WithRoles_WithoutAuthentication_ShouldReturnAuthenticationError() - { - // Arrange - var testRoles = "Viewer,Contributor"; - - // Act - var result = await _workspacesTool.ListWorkspacesSummaryAsync(testRoles); - - // Assert - AssertAuthenticationError(result); - } - [Fact] public void WorkspacesTool_ShouldBeRegisteredInDI() { @@ -183,55 +160,6 @@ public async Task ListWorkspacesAsync_WithAuthentication_AndPreferWorkspaceSpeci } } - [SkippableFact] - public async Task ListWorkspacesSummaryAsync_WithAuthentication_ShouldReturnSummaryFormat() - { - // Arrange - Try to authenticate - var isAuthenticated = await TryAuthenticateAsync(); - - Skip.IfNot(isAuthenticated, "Skipping authenticated test - no valid credentials available"); - - // Act - var result = await _workspacesTool.ListWorkspacesSummaryAsync(); - - // Assert - AssertResult("workspaces", result); - - // Additional check that summary format doesn't include API endpoints flag - if (IsValidJson(result) && !result.Contains("No workspaces found")) - { - var jsonDoc = JsonDocument.Parse(result); - Assert.False(jsonDoc.RootElement.TryGetProperty("includesApiEndpoints", out _)); - } - } - - [SkippableFact] - public async Task ListWorkspacesSummaryAsync_WithAuthentication_AndRoles_ShouldReturnFilteredSummary() - { - // Arrange - Try to authenticate - var isAuthenticated = await TryAuthenticateAsync(); - - Skip.IfNot(isAuthenticated, "Skipping authenticated test - no valid credentials available"); - - var testRoles = "Admin,Member"; - - // Act - var result = await _workspacesTool.ListWorkspacesSummaryAsync(testRoles); - - // Assert - AssertResult("workspaces", result); - - // Additional check for roles filtering in summary - 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_AllParameters_ShouldHandleComplexRequest() { diff --git a/DataFactory.MCP/Extensions/WorkspaceExtensions.cs b/DataFactory.MCP/Extensions/WorkspaceExtensions.cs index 46b7020..bfa0301 100644 --- a/DataFactory.MCP/Extensions/WorkspaceExtensions.cs +++ b/DataFactory.MCP/Extensions/WorkspaceExtensions.cs @@ -28,22 +28,4 @@ public static object ToFormattedInfo(this Workspace workspace) return formattedInfo; } - - /// - /// Formats a Workspace object for summary display (minimal information). - /// Used when displaying multiple workspaces in a list format. - /// - /// The workspace object to format - /// Formatted summary object - public static object ToSummaryInfo(this Workspace workspace) - { - return new - { - Id = workspace.Id, - DisplayName = workspace.DisplayName, - Type = workspace.Type.ToString(), - HasCapacity = !string.IsNullOrEmpty(workspace.CapacityId), - HasDescription = !string.IsNullOrEmpty(workspace.Description) - }; - } } \ No newline at end of file diff --git a/DataFactory.MCP/Tools/WorkspacesTool.cs b/DataFactory.MCP/Tools/WorkspacesTool.cs index b6ee05e..b2eb2f6 100644 --- a/DataFactory.MCP/Tools/WorkspacesTool.cs +++ b/DataFactory.MCP/Tools/WorkspacesTool.cs @@ -63,48 +63,4 @@ public async Task ListWorkspacesAsync( return string.Format(Messages.ErrorListingWorkspacesTemplate, ex.Message); } } - - [McpServerTool, Description(@"Gets a summary list of workspaces with essential information only. Useful for quick overview.")] - public async Task ListWorkspacesSummaryAsync( - [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) - { - try - { - var response = await _workspaceService.ListWorkspacesAsync(roles, continuationToken, false); - - if (!response.Value.Any()) - { - return Messages.NoWorkspacesFound; - } - - var result = new - { - TotalCount = response.Value.Count, - ContinuationToken = response.ContinuationToken, - HasMoreResults = !string.IsNullOrEmpty(response.ContinuationToken), - FilteredByRoles = !string.IsNullOrEmpty(roles), - Roles = roles, - Workspaces = response.Value.Select(w => w.ToSummaryInfo()) - }; - - 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/docs/workspace-management.md b/docs/workspace-management.md index 6c21137..f99f97b 100644 --- a/docs/workspace-management.md +++ b/docs/workspace-management.md @@ -6,7 +6,6 @@ This guide covers how to use the Microsoft Data Factory MCP Server for managing The workspace management tools allow you to: - List all accessible workspaces with filtering by role -- Retrieve summary information about workspaces - Work with different workspace types (Personal, Shared, Admin) - Navigate paginated results for large workspace collections @@ -58,42 +57,6 @@ list_workspaces(preferWorkspaceSpecificEndpoints: true) } ``` -### List Workspaces Summary - -Retrieve a summary list of workspaces with essential information only for quick overview. - -#### Usage -``` -list_workspaces_summary -``` - -#### With Role Filtering -``` -list_workspaces_summary(roles: "Admin,Contributor") -``` - -#### With Pagination -``` -list_workspaces_summary(continuationToken: "next-page-token") -``` - -#### Response Format -```json -{ - "totalCount": 10, - "continuationToken": "eyJza2lwIjoyMCwidGFrZSI6MjB9", - "hasMoreResults": true, - "workspaces": [ - { - "id": "12345678-1234-1234-1234-123456789012", - "name": "Sales Analytics Workspace", - "type": "PersonalGroup", - "state": "Active" - } - ] -} -``` - ## Role-Based Filtering The workspace tools support filtering by user roles within workspaces: @@ -109,7 +72,7 @@ The workspace tools support filtering by user roles within workspaces: list_workspaces(roles: "Admin,Member,Contributor") # Filter by single role -list_workspaces_summary(roles: "Admin") +list_workspaces(roles: "Admin") ``` ## Workspace Types @@ -130,9 +93,6 @@ The system supports different workspace types: # List workspaces where I'm an admin > list workspaces where I have admin role -# Get a summary of all workspaces -> give me a summary of my workspaces - # List workspaces with pagination > list my workspaces with continuation token abc123 ``` @@ -144,9 +104,6 @@ The system supports different workspace types: # List only active workspaces > show me all active workspaces - -# Get workspace summary for contributors -> give me a summary of workspaces where I'm a contributor ``` ### Pagination Scenarios From 6fe0266fd2e4ca646b89791eba7b4a86bce9ce8e Mon Sep 17 00:00:00 2001 From: ebramtawfik Date: Thu, 18 Sep 2025 15:27:07 -0700 Subject: [PATCH 08/11] removing unneaded dep --- .../DataFactory.MCP.Tests.csproj | 2 +- .../Abstractions/FabricServiceBase.cs | 10 +++++++--- DataFactory.MCP/DataFactory.MCP.csproj | 3 +-- DataFactory.MCP/Program.cs | 20 ++++--------------- .../Services/FabricConnectionService.cs | 3 +-- .../Services/FabricGatewayService.cs | 3 +-- .../Services/FabricWorkspaceService.cs | 3 +-- 7 files changed, 16 insertions(+), 28 deletions(-) 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/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/DataFactory.MCP.csproj b/DataFactory.MCP/DataFactory.MCP.csproj index 957d60c..9b0fc0d 100644 --- a/DataFactory.MCP/DataFactory.MCP.csproj +++ b/DataFactory.MCP/DataFactory.MCP.csproj @@ -30,9 +30,8 @@ - - + diff --git a/DataFactory.MCP/Program.cs b/DataFactory.MCP/Program.cs index d467919..8c7ca65 100644 --- a/DataFactory.MCP/Program.cs +++ b/DataFactory.MCP/Program.cs @@ -10,24 +10,12 @@ // 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 HTTP client and Fabric Workspace 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() 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 index bb4d9fa..dd86fcb 100644 --- a/DataFactory.MCP/Services/FabricWorkspaceService.cs +++ b/DataFactory.MCP/Services/FabricWorkspaceService.cs @@ -12,10 +12,9 @@ namespace DataFactory.MCP.Services; public class FabricWorkspaceService : FabricServiceBase, IFabricWorkspaceService { public FabricWorkspaceService( - HttpClient httpClient, ILogger logger, IAuthenticationService authService) - : base(httpClient, logger, authService) + : base(logger, authService) { } From e6baf27cc19ba9225cf01dda2fead842cd7143cf Mon Sep 17 00:00:00 2001 From: ebramtawfik Date: Thu, 18 Sep 2025 17:26:20 -0700 Subject: [PATCH 09/11] .WithAuthority(new Uri(AzureAdConfiguration.Authority)) --- DataFactory.MCP/Models/AzureAdConfiguration.cs | 2 +- DataFactory.MCP/Services/AuthenticationService.cs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DataFactory.MCP/Models/AzureAdConfiguration.cs b/DataFactory.MCP/Models/AzureAdConfiguration.cs index 8b11660..d02795c 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"; } 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); } From 247e0a5bbb2ece56404ed73af47361e17a91874c Mon Sep 17 00:00:00 2001 From: ebramtawfik Date: Fri, 19 Sep 2025 00:22:17 -0700 Subject: [PATCH 10/11] Fixing macOS auth --- DataFactory.MCP/DataFactory.MCP.csproj | 4 ++-- DataFactory.MCP/Models/AzureAdConfiguration.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DataFactory.MCP/DataFactory.MCP.csproj b/DataFactory.MCP/DataFactory.MCP.csproj index 9b0fc0d..581137a 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,7 +30,7 @@ - + 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"; } From eb47762fc9e1577a0cec1cf50ee31509812d741d Mon Sep 17 00:00:00 2001 From: ebramtawfik Date: Fri, 19 Sep 2025 00:30:33 -0700 Subject: [PATCH 11/11] no message --- DataFactory.MCP/DataFactory.MCP.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/DataFactory.MCP/DataFactory.MCP.csproj b/DataFactory.MCP/DataFactory.MCP.csproj index 581137a..bd603e4 100644 --- a/DataFactory.MCP/DataFactory.MCP.csproj +++ b/DataFactory.MCP/DataFactory.MCP.csproj @@ -30,6 +30,7 @@ +