diff --git a/.github/agents/devops.agent.md b/.github/agents/devops.agent.md new file mode 100644 index 00000000..b9840084 --- /dev/null +++ b/.github/agents/devops.agent.md @@ -0,0 +1,14 @@ +--- +description: "Use when: publishing packages, managing DevOps feed availability, troubleshooting CI failures, or working with Fabric.Mcp.Server integration." +name: "DevOps" +model: "Claude Opus 4.6 (copilot)" +tools: [execute, read, memory, todo] +user-invocable: true +argument-hint: "Describe the DevOps/CI task: publish package, check feed, fix CI failure" +--- + +You manage DevOps workflows, CI pipelines, and package publishing. You do NOT modify source code. + +Load the `devops.fabric-mcp-integration` skill for feed configuration, publishing workflow, and troubleshooting. + +Always run from the repo root. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 4c944a17..8f95c8d5 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -30,3 +30,5 @@ When these topics come up, reference the corresponding file in `docs/datafactory | Fast Copy, Action.Sequence, Modern Evaluator | `advanced.md` | For connection management (creating/listing connections, supported types, UI form), reference `docs/connection-management.md`. + +For DevOps/CI integration with Fabric.Mcp.Server (feed caching, publishing, trim safety), reference the `devops.fabric-mcp-integration` skill. diff --git a/.github/skills/devops.fabric-mcp-integration/SKILL.md b/.github/skills/devops.fabric-mcp-integration/SKILL.md new file mode 100644 index 00000000..77bd1724 --- /dev/null +++ b/.github/skills/devops.fabric-mcp-integration/SKILL.md @@ -0,0 +1,127 @@ +--- +name: devops.fabric-mcp-integration +description: "DevOps and CI integration with microsoft/mcp (Fabric.Mcp.Server). Use when publishing packages, managing feed availability, or troubleshooting CI failures." +--- + +# DevOps Integration with Fabric.Mcp.Server + +## Overview + +The `microsoft/mcp` repo consumes `Microsoft.DataFactory.MCP.Core` as a NuGet package +in `Fabric.Mcp.Tools.DataFactory`. CI builds use a DevOps feed with upstream caching +from nuget.org — packages are **not** immediately available after publishing. + +## Feed Configuration + +The Fabric.Mcp.Server `NuGet.config` uses a single feed: + +```xml + + + + +``` + +- **Feed UI:** https://dev.azure.com/azure-sdk/public/_artifacts/feed/azure-sdk-for-net +- **No direct nuget.org access** — all packages must come through this feed +- The feed has an **upstream** to nuget.org that caches packages on-demand + +## How Upstream Caching Works + +1. A new version is published to **nuget.org** +2. It is **NOT** immediately available on the DevOps feed +3. A Collaborator runs `dotnet restore` against the DevOps feed +4. The feed fetches and caches the package from nuget.org +5. Subsequent Fabric.Mcp.Server CI builds can then resolve it + +> **Important:** The `nuget list` search index can lag behind actual availability. +> If `dotnet restore` succeeds, the package IS available for CI even if `nuget list` doesn't show it yet. + +## After Publishing a New Version + +### Step 1: Verify on nuget.org + +```bash +nuget list Microsoft.DataFactory.MCP.Core \ + -Source "https://api.nuget.org/v3/index.json" \ + -PreRelease -AllVersions +``` + +### Step 2: Trigger the DevOps feed to cache it + +Run from the `microsoft/mcp` repo root: + +```bash +dotnet restore tools/Fabric.Mcp.Tools.DataFactory/src/Fabric.Mcp.Tools.DataFactory.csproj \ + --source "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-net/nuget/v3/index.json" +``` + +If restore succeeds, the package is cached and Fabric.Mcp.Server CI will find it. + +### Step 3: Verify (optional) + +```bash +nuget list Microsoft.DataFactory.MCP.Core \ + -Source "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-net/nuget/v3/index.json" \ + -PreRelease -AllVersions +``` + +> The search index may lag — restore success is the definitive proof. + +## Version Management + +### DataFactory.MCP side (this repo) + +Version is in `Directory.Build.props`: + +```xml +0 +20 +0 +beta +``` + +### Fabric.Mcp.Server side (consumer repo) + +Version is in `Directory.Packages.props` (Central Package Management): + +```xml + +``` + +The project reference is in: +`tools/Fabric.Mcp.Tools.DataFactory/src/Fabric.Mcp.Tools.DataFactory.csproj` + +## Credential Provider Setup + +To authenticate to the DevOps feed locally, install the credential provider: +https://go.microsoft.com/fwlink/?linkid=2099625 + +Once installed, `dotnet restore` triggers an auth challenge and grants Collaborator access. +The feed is public — anyone can pull packages that are already cached. +Collaborators can also trigger the feed to ingest new packages from nuget.org. + +## Trim Safety Requirements + +Fabric.Mcp.Server publishes with IL trimming enabled: + +```bash +dotnet publish servers/Fabric.Mcp.Server/src/Fabric.Mcp.Server.csproj \ + --runtime linux-x64 --self-contained \ + /p:PublishTrimmed=true /p:PublishSingleFile=true /p:TreatWarningsAsErrors=true +``` + +All code consumed from `Microsoft.DataFactory.MCP.Core` must be trim-safe: +- Use source-generated JSON (`DataFactoryJsonContext`) instead of reflection-based serialization +- Mark reflection-dependent APIs with `[RequiresUnreferencedCode]` +- Test with the publish command above before submitting version bumps + +## Troubleshooting + +| Issue | Cause | Fix | +|-------|-------|-----| +| `NU1102: Unable to find package` | Version not cached on DevOps feed | Run `dotnet restore` locally to trigger upstream cache | +| `nuget list` doesn't show new version | Search index lag | Check with `dotnet restore` instead — success = available | +| 401 Unauthorized on restore | Missing credential provider | Install cred provider (link above) | +| IL2026 trim warnings on publish | Reflection-based JSON in consumed code | Use `DataFactoryJsonContext` source-gen JSON | diff --git a/.pipelines/pack-and-sign-nugets.yaml b/.pipelines/pack-and-sign-nugets.yaml index 501ff345..ca07420c 100644 --- a/.pipelines/pack-and-sign-nugets.yaml +++ b/.pipelines/pack-and-sign-nugets.yaml @@ -15,6 +15,7 @@ trigger: paths: include: - DataFactory.MCP/* + - DataFactory.MCP.Core/* pr: branches: @@ -24,6 +25,7 @@ pr: paths: include: - DataFactory.MCP/* + - DataFactory.MCP.Core/* parameters: - name: "debug" @@ -155,6 +157,45 @@ extends: Get-ChildItem "$mcpAppsDir/dist" -Recurse | Select-Object FullName | Format-Table -AutoSize | Out-String | Write-Host pwsh: true + # --- DataFactory.MCP.Core (library package) --- + - task: DotNetCoreCLI@2 + displayName: "Restore Core packages" + inputs: + command: "restore" + projects: "$(Build.SourcesDirectory)/DataFactory.MCP.Core/DataFactory.MCP.Core.csproj" + feedsToUse: "config" + nugetConfigPath: "$(Build.SourcesDirectory)/nuget.private.config" + verbosityRestore: "Normal" + noCache: true + retryCountOnTaskFailure: 3 + + - task: DotNetCoreCLI@2 + displayName: "Build Core project" + inputs: + command: "build" + projects: "$(Build.SourcesDirectory)/DataFactory.MCP.Core/DataFactory.MCP.Core.csproj" + arguments: "--configuration $(BuildConfiguration) --no-restore" + + - task: onebranch.pipeline.signing@1 + displayName: "Sign Core binary files" + inputs: + command: "sign" + signing_profile: "external_distribution" + files_to_sign: | + **/DataFactory.MCP.Core.dll; + search_root: "$(Build.SourcesDirectory)\\DataFactory.MCP.Core\\bin\\$(BuildConfiguration)\\net10.0" + + - task: DotNetCoreCLI@2 + displayName: "Pack Core NuGet package" + inputs: + command: "pack" + packagesToPack: "$(Build.SourcesDirectory)/DataFactory.MCP.Core/DataFactory.MCP.Core.csproj" + configuration: "$(BuildConfiguration)" + outputDir: "$(OUTPUTROOT)" + nobuild: true + includeSymbols: true + + # --- DataFactory.MCP (tool package) --- - task: DotNetCoreCLI@2 displayName: "Restore packages" inputs: diff --git a/DataFactory.MCP.Core/Abstractions/FabricServiceBase.cs b/DataFactory.MCP.Core/Abstractions/FabricServiceBase.cs index cb1ff518..941fbd98 100644 --- a/DataFactory.MCP.Core/Abstractions/FabricServiceBase.cs +++ b/DataFactory.MCP.Core/Abstractions/FabricServiceBase.cs @@ -20,6 +20,15 @@ public abstract class FabricServiceBase protected readonly IValidationService ValidationService; protected static JsonSerializerOptions JsonOptions => JsonSerializerOptionsProvider.FabricApi; + /// + /// Serializes a request object using source-generated JSON context for AOT compatibility. + /// All request types must be registered in DataFactoryJsonContext. + /// + private static string SerializeRequest(object request) + { + return JsonSerializer.Serialize(request, request.GetType(), DataFactoryJsonContext.Default); + } + protected FabricServiceBase( IHttpClientFactory httpClientFactory, ILogger logger, @@ -61,7 +70,7 @@ protected void ValidateGuids(params (string value, string name)[] guids) .Build(); Logger.LogInformation("Posting to: {Url}", url); - var jsonContent = JsonSerializer.Serialize(request, JsonOptions); + var jsonContent = SerializeRequest(request); Logger.LogDebug("Request body: {Body}", jsonContent); var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); @@ -79,7 +88,7 @@ protected void ValidateGuids(params (string value, string name)[] guids) .Build(); Logger.LogInformation("Patching: {Url}", url); - var jsonContent = JsonSerializer.Serialize(request, JsonOptions); + var jsonContent = SerializeRequest(request); var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); var httpRequest = new HttpRequestMessage(HttpMethod.Patch, url) { Content = content }; @@ -97,7 +106,7 @@ protected async Task PostAsBytesAsync(string endpoint, object request) .Build(); Logger.LogInformation("Posting to: {Url}", url); - var jsonContent = JsonSerializer.Serialize(request, JsonOptions); + var jsonContent = SerializeRequest(request); var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); var response = await HttpClient.PostAsync(url, content); @@ -115,7 +124,7 @@ protected async Task PostAsBytesAsync(string endpoint, object request) .Build(); Logger.LogInformation("Posting to: {Url}", url); - var jsonContent = request != null ? JsonSerializer.Serialize(request, JsonOptions) : null; + var jsonContent = request != null ? SerializeRequest(request) : null; var content = jsonContent != null ? new StringContent(jsonContent, Encoding.UTF8, "application/json") : null; var response = await HttpClient.PostAsync(url, content); @@ -135,7 +144,7 @@ protected async Task PostNoContentAsync(string endpoint, object request) .Build(); Logger.LogInformation("Posting to: {Url}", url); - var jsonContent = JsonSerializer.Serialize(request, JsonOptions); + var jsonContent = SerializeRequest(request); var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); var response = await HttpClient.PostAsync(url, content); @@ -145,9 +154,9 @@ protected async Task PostNoContentAsync(string endpoint, object request) return true; } - var (_, _, error) = await response.TryReadAsJsonAsync(JsonOptions); + var errorContent = await response.Content.ReadAsStringAsync(); Logger.LogError("API POST request failed. Status: {StatusCode}, Content: {Content}", - error?.StatusCode, error?.ResponseContent); + response.StatusCode, errorContent); return false; } } diff --git a/DataFactory.MCP.Core/Abstractions/Interfaces/IFabricCopyJobService.cs b/DataFactory.MCP.Core/Abstractions/Interfaces/IFabricCopyJobService.cs index fb1e8710..23d15002 100644 --- a/DataFactory.MCP.Core/Abstractions/Interfaces/IFabricCopyJobService.cs +++ b/DataFactory.MCP.Core/Abstractions/Interfaces/IFabricCopyJobService.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using DataFactory.MCP.Models.CopyJob; using DataFactory.MCP.Models.CopyJob.Definition; using DataFactory.MCP.Models.Pipeline; @@ -60,7 +61,7 @@ Task UpdateCopyJobDefinitionAsync( Task RunCopyJobAsync( string workspaceId, string copyJobId, - object? executionData = null); + JsonElement? executionData = null); /// /// Gets the status of a copy job instance diff --git a/DataFactory.MCP.Core/Abstractions/Interfaces/IFabricPipelineService.cs b/DataFactory.MCP.Core/Abstractions/Interfaces/IFabricPipelineService.cs index e27811c7..be388035 100644 --- a/DataFactory.MCP.Core/Abstractions/Interfaces/IFabricPipelineService.cs +++ b/DataFactory.MCP.Core/Abstractions/Interfaces/IFabricPipelineService.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using DataFactory.MCP.Models.Pipeline; using DataFactory.MCP.Models.Pipeline.Definition; using DataFactory.MCP.Models.Pipeline.Schedule; @@ -59,7 +60,7 @@ Task UpdatePipelineDefinitionAsync( Task RunPipelineAsync( string workspaceId, string pipelineId, - object? executionData = null); + JsonElement? executionData = null); /// /// Gets the status of a pipeline job instance diff --git a/DataFactory.MCP.Core/Configuration/DataFactoryJsonContext.cs b/DataFactory.MCP.Core/Configuration/DataFactoryJsonContext.cs new file mode 100644 index 00000000..b6492477 --- /dev/null +++ b/DataFactory.MCP.Core/Configuration/DataFactoryJsonContext.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using DataFactory.MCP.Models.Capacity; +using DataFactory.MCP.Models.Common; +using DataFactory.MCP.Models.Connection; +using DataFactory.MCP.Models.CopyJob; +using DataFactory.MCP.Models.CopyJob.Definition; +using DataFactory.MCP.Models.Dataflow; +using DataFactory.MCP.Models.Dataflow.Definition; +using DataFactory.MCP.Models.Dataflow.Query; +using DataFactory.MCP.Models.Gateway; +using DataFactory.MCP.Models.Pipeline; +using DataFactory.MCP.Models.Pipeline.Definition; +using DataFactory.MCP.Models.Pipeline.Schedule; +using DataFactory.MCP.Models.Workspace; +using DataFactory.MCP.Services.DMTSv2; + +namespace DataFactory.MCP.Configuration; + +[JsonSourceGenerationOptions( + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] +// Connection types (concrete subtypes only - base type has custom converter) +[JsonSerializable(typeof(ShareableCloudConnection))] +[JsonSerializable(typeof(PersonalCloudConnection))] +[JsonSerializable(typeof(OnPremisesGatewayConnection))] +[JsonSerializable(typeof(OnPremisesGatewayPersonalConnection))] +[JsonSerializable(typeof(VirtualNetworkGatewayConnection))] +[JsonSerializable(typeof(ListConnectionsResponse))] +[JsonSerializable(typeof(CreateConnectionRequest))] +[JsonSerializable(typeof(ListSupportedConnectionTypesResponse))] +// Gateway types (concrete subtypes only - base type has custom converter) +[JsonSerializable(typeof(OnPremisesGateway))] +[JsonSerializable(typeof(OnPremisesGatewayPersonal))] +[JsonSerializable(typeof(VirtualNetworkGateway))] +[JsonSerializable(typeof(ListGatewaysResponse))] +[JsonSerializable(typeof(CreateVirtualnetworkGatewayRequest))] +[JsonSerializable(typeof(CreateVirtualnetworkGatewayResponse))] +// Capacity types +[JsonSerializable(typeof(Capacity))] +[JsonSerializable(typeof(ListCapacitiesResponse))] +// Workspace types +[JsonSerializable(typeof(Workspace))] +[JsonSerializable(typeof(ListWorkspacesResponse))] +// CopyJob types +[JsonSerializable(typeof(CopyJob))] +[JsonSerializable(typeof(CreateCopyJobRequest))] +[JsonSerializable(typeof(CreateCopyJobResponse))] +[JsonSerializable(typeof(UpdateCopyJobRequest))] +[JsonSerializable(typeof(ListCopyJobsResponse))] +// CopyJob Definition types +[JsonSerializable(typeof(CopyJobDefinition))] +[JsonSerializable(typeof(CopyJobDefinitionPart))] +[JsonSerializable(typeof(UpdateCopyJobDefinitionRequest))] +[JsonSerializable(typeof(GetCopyJobDefinitionResponse))] +// Pipeline types +[JsonSerializable(typeof(Pipeline))] +[JsonSerializable(typeof(CreatePipelineRequest))] +[JsonSerializable(typeof(CreatePipelineResponse))] +[JsonSerializable(typeof(UpdatePipelineRequest))] +[JsonSerializable(typeof(ListPipelinesResponse))] +[JsonSerializable(typeof(Models.Pipeline.ItemJobInstance), TypeInfoPropertyName = "PipelineItemJobInstance")] +// Pipeline Schedule types +[JsonSerializable(typeof(CreateScheduleRequest))] +[JsonSerializable(typeof(ItemSchedule))] +[JsonSerializable(typeof(ListSchedulesResponse))] +// Pipeline Definition types +[JsonSerializable(typeof(PipelineDefinition))] +[JsonSerializable(typeof(PipelineDefinitionPart))] +[JsonSerializable(typeof(UpdatePipelineDefinitionRequest))] +[JsonSerializable(typeof(UpdatePipelineDefinitionResponse))] +[JsonSerializable(typeof(GetPipelineDefinitionResponse))] +// Dataflow types +[JsonSerializable(typeof(Dataflow))] +[JsonSerializable(typeof(CreateDataflowRequest))] +[JsonSerializable(typeof(CreateDataflowResponse))] +[JsonSerializable(typeof(ListDataflowsResponse))] +// Dataflow Query types +[JsonSerializable(typeof(ExecuteDataflowQueryRequest))] +[JsonSerializable(typeof(ExecuteDataflowQueryResponse))] +// Dataflow BackgroundTask types +[JsonSerializable(typeof(DataFactory.MCP.Models.Dataflow.BackgroundTask.RunOnDemandExecuteRequest))] +[JsonSerializable(typeof(DataFactory.MCP.Models.Dataflow.BackgroundTask.ItemJobInstance), TypeInfoPropertyName = "DataflowItemJobInstance")] +// Dataflow Definition types +[JsonSerializable(typeof(DataflowDefinition))] +[JsonSerializable(typeof(DataflowDefinitionPart))] +[JsonSerializable(typeof(GetDataflowDefinitionHttpResponse))] +[JsonSerializable(typeof(UpdateDataflowDefinitionRequest))] +[JsonSerializable(typeof(UpdateDataflowDefinitionResponse))] +// Gateway Cluster Datasource types (DMTSv2) +[JsonSerializable(typeof(GatewayClusterDatasourceService.GatewayClusterDatasourcesResponse))] +[JsonSerializable(typeof(GatewayClusterDatasourceService.CloudDatasourceInfo))] +// Common types +[JsonSerializable(typeof(JsonElement))] +[JsonSerializable(typeof(EmptyRequest))] +[JsonSerializable(typeof(RunOnDemandRequest))] +internal sealed partial class DataFactoryJsonContext : JsonSerializerContext +{ +} diff --git a/DataFactory.MCP.Core/Configuration/JsonSerializerOptionsProvider.cs b/DataFactory.MCP.Core/Configuration/JsonSerializerOptionsProvider.cs index 7e66fa9d..8fd7d55b 100644 --- a/DataFactory.MCP.Core/Configuration/JsonSerializerOptionsProvider.cs +++ b/DataFactory.MCP.Core/Configuration/JsonSerializerOptionsProvider.cs @@ -23,7 +23,6 @@ public static class JsonSerializerOptionsProvider DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, Converters = { - new JsonStringEnumConverter(), new ConnectionJsonConverter(), new GatewayJsonConverter() } diff --git a/DataFactory.MCP.Core/DataFactory.MCP.Core.csproj b/DataFactory.MCP.Core/DataFactory.MCP.Core.csproj index ffb90496..dcec22d7 100644 --- a/DataFactory.MCP.Core/DataFactory.MCP.Core.csproj +++ b/DataFactory.MCP.Core/DataFactory.MCP.Core.csproj @@ -10,6 +10,10 @@ Microsoft.DataFactory.MCP.Core AI; MCP; server; library Core library for DataFactory MCP server - contains services, tools, and models. + true + true + true + $(NoWarn);IL2026;IL3050;IL2091 Microsoft-Fabric.png README.md @@ -27,6 +31,7 @@ + diff --git a/DataFactory.MCP.Core/Extensions/HttpResponseMessageExtensions.cs b/DataFactory.MCP.Core/Extensions/HttpResponseMessageExtensions.cs index 256c78e0..efeb2b0c 100644 --- a/DataFactory.MCP.Core/Extensions/HttpResponseMessageExtensions.cs +++ b/DataFactory.MCP.Core/Extensions/HttpResponseMessageExtensions.cs @@ -64,7 +64,7 @@ public static class HttpResponseMessageExtensions return null; } - return JsonSerializer.Deserialize(content, options ?? JsonSerializerOptionsProvider.FabricApi); + return (T?)JsonSerializer.Deserialize(content, typeof(T), DataFactoryJsonContext.Default); } /// @@ -95,7 +95,7 @@ public static async Task ReadAsJsonOrDefaultAsync( return defaultValue; } - return JsonSerializer.Deserialize(content, options ?? JsonSerializerOptionsProvider.FabricApi) + return (T?)JsonSerializer.Deserialize(content, typeof(T), DataFactoryJsonContext.Default) ?? defaultValue; } @@ -153,7 +153,7 @@ public static async Task EnsureSuccessOrThrowAsync( } var content = await response.Content.ReadAsStringAsync(cancellationToken); - var value = JsonSerializer.Deserialize(content, options ?? JsonSerializerOptionsProvider.FabricApi); + var value = (T?)JsonSerializer.Deserialize(content, typeof(T), DataFactoryJsonContext.Default); return (true, value, null); } diff --git a/DataFactory.MCP.Core/Extensions/JsonExtensions.cs b/DataFactory.MCP.Core/Extensions/JsonExtensions.cs index bf9a2ad0..c6e72b18 100644 --- a/DataFactory.MCP.Core/Extensions/JsonExtensions.cs +++ b/DataFactory.MCP.Core/Extensions/JsonExtensions.cs @@ -1,4 +1,5 @@ using DataFactory.MCP.Configuration; +using System.Diagnostics.CodeAnalysis; using System.Text.Json; namespace DataFactory.MCP.Extensions; @@ -9,10 +10,12 @@ namespace DataFactory.MCP.Extensions; public static class JsonExtensions { /// - /// Serializes an object to JSON using consistent MCP formatting + /// Serializes an object to JSON using consistent MCP formatting. + /// Uses reflection-based serialization to support anonymous types and dynamic MCP responses. /// /// The object to serialize /// The JSON string representation + [RequiresUnreferencedCode("MCP response serialization may use reflection for formatting")] public static string ToMcpJson(this object obj) { return JsonSerializer.Serialize(obj, JsonSerializerOptionsProvider.McpResponse); diff --git a/DataFactory.MCP.Core/Extensions/ServiceCollectionExtensions.cs b/DataFactory.MCP.Core/Extensions/ServiceCollectionExtensions.cs index 0f9483a1..9745f4c7 100644 --- a/DataFactory.MCP.Core/Extensions/ServiceCollectionExtensions.cs +++ b/DataFactory.MCP.Core/Extensions/ServiceCollectionExtensions.cs @@ -1,4 +1,6 @@ +using Azure.Core; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using DataFactory.MCP.Abstractions.Interfaces; @@ -15,6 +17,8 @@ using DataFactory.MCP.Tools.Dataflow; using DataFactory.MCP.Tools.CopyJob; using DataFactory.MCP.Tools.Pipeline; +using DataFactory.MCP.Handlers.Pipeline; +using DataFactory.MCP.Handlers.Dataflow; namespace DataFactory.MCP.Extensions; @@ -47,15 +51,29 @@ public static IServiceCollection AddDataFactoryMcpServices(this IServiceCollecti }).AddHttpMessageHandler(); // Register core services + services.AddSingleton(); + + // Authentication system with providers (needed for standalone mode) + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + // If host already provides TokenCredential (e.g., Fabric MCP Server), + // use TokenCredentialAuthenticationService; otherwise use standalone AuthenticationService + services.TryAddSingleton(sp => + { + var credential = sp.GetService(); + if (credential != null) + { + var logger = sp.GetRequiredService>(); + return new TokenCredentialAuthenticationService(credential, logger); + } + return ActivatorUtilities.CreateInstance(sp); + }); + + // Other services services - .AddSingleton() - // Authentication system with providers - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - .AddSingleton() - // Other services .AddSingleton() .AddSingleton() .AddSingleton() @@ -67,6 +85,11 @@ public static IServiceCollection AddDataFactoryMcpServices(this IServiceCollecti .AddSingleton() // Pipeline service .AddSingleton() + // Pipeline handlers (shared handler pattern) + .AddSingleton() + // Dataflow handlers + .AddSingleton() + .AddSingleton() // Copy Job service .AddSingleton() // Session accessor for background notifications diff --git a/DataFactory.MCP.Core/Handlers/Dataflow/DataflowHandler.cs b/DataFactory.MCP.Core/Handlers/Dataflow/DataflowHandler.cs new file mode 100644 index 00000000..c4071427 --- /dev/null +++ b/DataFactory.MCP.Core/Handlers/Dataflow/DataflowHandler.cs @@ -0,0 +1,89 @@ +using DataFactory.MCP.Abstractions.Interfaces; +using DataFactory.MCP.Models; +using DataFactory.MCP.Models.Dataflow; + +namespace DataFactory.MCP.Handlers.Dataflow; + +public record ListDataflowsResult( + string WorkspaceId, + int DataflowCount, + string? ContinuationToken, + string? ContinuationUri, + bool HasMoreResults, + IReadOnlyList Dataflows); + +public record CreateDataflowResult(CreateDataflowResponse Dataflow); + +public class DataflowHandler(IFabricDataflowService dataflowService) +{ + public async Task> ListAsync(string workspaceId, string? continuationToken = null) + { + if (string.IsNullOrWhiteSpace(workspaceId)) + return ToolResult.Failure(Messages.InvalidParameterEmpty("workspaceId"), "validation"); + + try + { + var response = await dataflowService.ListDataflowsAsync(workspaceId, continuationToken); + var result = new ListDataflowsResult( + WorkspaceId: workspaceId, + DataflowCount: response.Value.Count, + ContinuationToken: response.ContinuationToken, + ContinuationUri: response.ContinuationUri, + HasMoreResults: !string.IsNullOrEmpty(response.ContinuationToken), + Dataflows: response.Value); + return ToolResult.Success(result); + } + catch (UnauthorizedAccessException ex) + { + return ToolResult.Failure(string.Format(Messages.AuthenticationErrorTemplate, ex.Message), "auth"); + } + catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + return ToolResult.Failure("Authentication failed. Please check your credentials.", "auth"); + } + catch (HttpRequestException ex) + { + return ToolResult.Failure($"Failed to list dataflows: {ex.Message}", "http"); + } + catch (Exception ex) + { + return ToolResult.Failure($"Unexpected error listing dataflows: {ex.Message}", "operation"); + } + } + + public async Task> CreateAsync(string workspaceId, string displayName, string? description = null, string? folderId = null) + { + if (string.IsNullOrWhiteSpace(workspaceId)) + return ToolResult.Failure(Messages.InvalidParameterEmpty("workspaceId"), "validation"); + if (string.IsNullOrWhiteSpace(displayName)) + return ToolResult.Failure(Messages.InvalidParameterEmpty("displayName"), "validation"); + + try + { + var request = new CreateDataflowRequest + { + DisplayName = displayName, + Description = description, + FolderId = folderId + }; + var dataflow = await dataflowService.CreateDataflowAsync(workspaceId, request); + return ToolResult.Success(new CreateDataflowResult(dataflow)); + } + catch (UnauthorizedAccessException ex) + { + return ToolResult.Failure(string.Format(Messages.AuthenticationErrorTemplate, ex.Message), "auth"); + } + catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + return ToolResult.Failure("Authentication failed. Please check your credentials.", "auth"); + } + catch (HttpRequestException ex) + { + return ToolResult.Failure($"Failed to create dataflow: {ex.Message}", "http"); + } + catch (Exception ex) + { + return ToolResult.Failure($"Unexpected error creating dataflow: {ex.Message}", "operation"); + } + } +} diff --git a/DataFactory.MCP.Core/Handlers/Dataflow/DataflowQueryHandler.cs b/DataFactory.MCP.Core/Handlers/Dataflow/DataflowQueryHandler.cs new file mode 100644 index 00000000..4147162b --- /dev/null +++ b/DataFactory.MCP.Core/Handlers/Dataflow/DataflowQueryHandler.cs @@ -0,0 +1,79 @@ +using DataFactory.MCP.Abstractions.Interfaces; +using DataFactory.MCP.Extensions; +using DataFactory.MCP.Models; +using DataFactory.MCP.Models.Dataflow.Query; + +namespace DataFactory.MCP.Handlers.Dataflow; + +public record ExecuteQueryResult( + bool Success, + object? Data, + QueryResultSummary? Summary); + +public class DataflowQueryHandler(IFabricDataflowService dataflowService) +{ + public async Task> ExecuteQueryAsync( + string workspaceId, + string dataflowId, + string queryName, + string customMashupDocument) + { + if (string.IsNullOrWhiteSpace(workspaceId)) + return ToolResult.Failure(Messages.InvalidParameterEmpty("workspaceId"), "validation"); + if (string.IsNullOrWhiteSpace(dataflowId)) + return ToolResult.Failure(Messages.InvalidParameterEmpty("dataflowId"), "validation"); + if (string.IsNullOrWhiteSpace(queryName)) + return ToolResult.Failure(Messages.InvalidParameterEmpty("queryName"), "validation"); + if (string.IsNullOrWhiteSpace(customMashupDocument)) + return ToolResult.Failure(Messages.InvalidParameterEmpty("customMashupDocument"), "validation"); + + try + { + // Auto-wrap the query if it's not already in section format + var wrappedQuery = customMashupDocument.WrapForDataflowQuery(queryName); + + var request = new ExecuteDataflowQueryRequest + { + QueryName = queryName, + CustomMashupDocument = wrappedQuery + }; + + var response = await dataflowService.ExecuteQueryAsync(workspaceId, dataflowId, request); + + if (!response.Success) + { + return ToolResult.Failure( + $"Query execution failed for '{queryName}' in dataflow {dataflowId}: {response.Error}", + "operation"); + } + + var data = response.CreateArrowDataReport(); + var result = new ExecuteQueryResult( + Success: true, + Data: data, + Summary: response.Summary); + + return ToolResult.Success(result); + } + catch (UnauthorizedAccessException ex) + { + return ToolResult.Failure( + string.Format(Messages.AuthenticationErrorTemplate, ex.Message), "auth"); + } + catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + return ToolResult.Failure( + "Authentication failed. Please check your credentials.", "auth"); + } + catch (HttpRequestException ex) + { + return ToolResult.Failure( + $"Failed to execute dataflow query: {ex.Message}", "http"); + } + catch (Exception ex) + { + return ToolResult.Failure( + $"Unexpected error executing dataflow query: {ex.Message}", "operation"); + } + } +} diff --git a/DataFactory.MCP.Core/Handlers/Pipeline/PipelineHandler.cs b/DataFactory.MCP.Core/Handlers/Pipeline/PipelineHandler.cs new file mode 100644 index 00000000..a2c5d09f --- /dev/null +++ b/DataFactory.MCP.Core/Handlers/Pipeline/PipelineHandler.cs @@ -0,0 +1,163 @@ +using System.Text.Json; +using DataFactory.MCP.Abstractions.Interfaces; +using DataFactory.MCP.Models; +using DataFactory.MCP.Models.Pipeline; + +namespace DataFactory.MCP.Handlers.Pipeline; + +public record ListPipelinesResult( + string WorkspaceId, + int PipelineCount, + string? ContinuationToken, + string? ContinuationUri, + bool HasMoreResults, + IReadOnlyList Pipelines); + +public record CreatePipelineResult(CreatePipelineResponse Pipeline); + +public record GetPipelineResult(Models.Pipeline.Pipeline Pipeline); + +public record RunPipelineResult(string? LocationUrl, string? JobInstanceId); + +public class PipelineHandler(IFabricPipelineService pipelineService) +{ + public async Task> ListAsync(string workspaceId, string? continuationToken = null) + { + if (string.IsNullOrWhiteSpace(workspaceId)) + return ToolResult.Failure(Messages.InvalidParameterEmpty("workspaceId"), "validation"); + + try + { + var response = await pipelineService.ListPipelinesAsync(workspaceId, continuationToken); + var result = new ListPipelinesResult( + WorkspaceId: workspaceId, + PipelineCount: response.Value.Count, + ContinuationToken: response.ContinuationToken, + ContinuationUri: response.ContinuationUri, + HasMoreResults: !string.IsNullOrEmpty(response.ContinuationToken), + Pipelines: response.Value); + return ToolResult.Success(result); + } + catch (UnauthorizedAccessException ex) + { + return ToolResult.Failure(string.Format(Messages.AuthenticationErrorTemplate, ex.Message), "auth"); + } + catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + return ToolResult.Failure(string.Format(Messages.AuthenticationErrorTemplate, ex.Message), "auth"); + } + catch (HttpRequestException ex) + { + return ToolResult.Failure($"Failed to list pipelines: {ex.Message}", "http"); + } + catch (Exception ex) + { + return ToolResult.Failure($"Unexpected error listing pipelines: {ex.Message}", "operation"); + } + } + + public async Task> CreateAsync(string workspaceId, string displayName, string? description = null, string? folderId = null) + { + if (string.IsNullOrWhiteSpace(workspaceId)) + return ToolResult.Failure(Messages.InvalidParameterEmpty("workspaceId"), "validation"); + if (string.IsNullOrWhiteSpace(displayName)) + return ToolResult.Failure(Messages.InvalidParameterEmpty("displayName"), "validation"); + + try + { + var request = new CreatePipelineRequest + { + DisplayName = displayName, + Description = description, + FolderId = folderId + }; + var pipeline = await pipelineService.CreatePipelineAsync(workspaceId, request); + return ToolResult.Success(new CreatePipelineResult(pipeline)); + } + catch (UnauthorizedAccessException ex) + { + return ToolResult.Failure(string.Format(Messages.AuthenticationErrorTemplate, ex.Message), "auth"); + } + catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + return ToolResult.Failure(string.Format(Messages.AuthenticationErrorTemplate, ex.Message), "auth"); + } + catch (HttpRequestException ex) + { + return ToolResult.Failure($"Failed to create pipeline: {ex.Message}", "http"); + } + catch (Exception ex) + { + return ToolResult.Failure($"Unexpected error creating pipeline: {ex.Message}", "operation"); + } + } + + public async Task> GetAsync(string workspaceId, string pipelineId) + { + if (string.IsNullOrWhiteSpace(workspaceId)) + return ToolResult.Failure(Messages.InvalidParameterEmpty("workspaceId"), "validation"); + if (string.IsNullOrWhiteSpace(pipelineId)) + return ToolResult.Failure(Messages.InvalidParameterEmpty("pipelineId"), "validation"); + + try + { + var pipeline = await pipelineService.GetPipelineAsync(workspaceId, pipelineId); + return ToolResult.Success(new GetPipelineResult(pipeline)); + } + catch (UnauthorizedAccessException ex) + { + return ToolResult.Failure(string.Format(Messages.AuthenticationErrorTemplate, ex.Message), "auth"); + } + catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + return ToolResult.Failure(string.Format(Messages.AuthenticationErrorTemplate, ex.Message), "auth"); + } + catch (HttpRequestException ex) + { + return ToolResult.Failure($"Failed to get pipeline: {ex.Message}", "http"); + } + catch (Exception ex) + { + return ToolResult.Failure($"Unexpected error getting pipeline: {ex.Message}", "operation"); + } + } + + public async Task> RunAsync(string workspaceId, string pipelineId, JsonElement? executionData = null) + { + if (string.IsNullOrWhiteSpace(workspaceId)) + return ToolResult.Failure(Messages.InvalidParameterEmpty("workspaceId"), "validation"); + if (string.IsNullOrWhiteSpace(pipelineId)) + return ToolResult.Failure(Messages.InvalidParameterEmpty("pipelineId"), "validation"); + + try + { + var location = await pipelineService.RunPipelineAsync(workspaceId, pipelineId, executionData); + + // Extract job instance ID from the Location header URL + string? jobInstanceId = null; + if (!string.IsNullOrEmpty(location)) + { + var segments = new Uri(location).Segments; + jobInstanceId = segments.LastOrDefault()?.TrimEnd('/'); + } + + return ToolResult.Success(new RunPipelineResult(location, jobInstanceId)); + } + catch (UnauthorizedAccessException ex) + { + return ToolResult.Failure(string.Format(Messages.AuthenticationErrorTemplate, ex.Message), "auth"); + } + catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + return ToolResult.Failure(string.Format(Messages.AuthenticationErrorTemplate, ex.Message), "auth"); + } + catch (HttpRequestException ex) + { + return ToolResult.Failure($"Failed to run pipeline: {ex.Message}", "http"); + } + catch (Exception ex) + { + return ToolResult.Failure($"Unexpected error running pipeline: {ex.Message}", "operation"); + } + } +} diff --git a/DataFactory.MCP.Core/Handlers/ToolResult.cs b/DataFactory.MCP.Core/Handlers/ToolResult.cs new file mode 100644 index 00000000..438c9ed7 --- /dev/null +++ b/DataFactory.MCP.Core/Handlers/ToolResult.cs @@ -0,0 +1,17 @@ +namespace DataFactory.MCP.Handlers; + +/// +/// Represents the result of a tool handler execution. +/// Framework-agnostic — both SDK tools and Fabric commands consume this. +/// +public class ToolResult +{ + public bool IsSuccess { get; init; } + public T? Value { get; init; } + public string? Error { get; init; } + public string? ErrorType { get; init; } // "validation", "auth", "http", "operation" + + public static ToolResult Success(T value) => new() { IsSuccess = true, Value = value }; + public static ToolResult Failure(string error, string errorType = "operation") + => new() { IsSuccess = false, Error = error, ErrorType = errorType }; +} diff --git a/DataFactory.MCP.Core/Handlers/ToolResultExtensions.cs b/DataFactory.MCP.Core/Handlers/ToolResultExtensions.cs new file mode 100644 index 00000000..ad11e5b5 --- /dev/null +++ b/DataFactory.MCP.Core/Handlers/ToolResultExtensions.cs @@ -0,0 +1,23 @@ +using DataFactory.MCP.Models.Common.Responses.Errors; + +namespace DataFactory.MCP.Handlers; + +/// +/// Extension methods for converting ToolResult failures to MCP error responses. +/// +public static class ToolResultExtensions +{ + /// + /// Converts a failed ToolResult to the appropriate MCP error response object. + /// + public static object ToErrorResponse(this ToolResult result, string operation = "executing tool") + { + return result.ErrorType switch + { + "validation" => new McpValidationErrorResponse(result.Error ?? "Validation failed"), + "auth" => new McpAuthenticationErrorResponse(result.Error ?? "Authentication failed"), + "http" => new McpHttpErrorResponse(result.Error ?? "HTTP request failed"), + _ => new McpOperationErrorResponse(result.Error ?? "Unknown error", operation), + }; + } +} diff --git a/DataFactory.MCP.Core/Models/Capacity/Capacity.cs b/DataFactory.MCP.Core/Models/Capacity/Capacity.cs index 6d0256e7..1746ef10 100644 --- a/DataFactory.MCP.Core/Models/Capacity/Capacity.cs +++ b/DataFactory.MCP.Core/Models/Capacity/Capacity.cs @@ -35,6 +35,6 @@ public class Capacity /// The capacity state /// [JsonPropertyName("state")] - [JsonConverter(typeof(JsonStringEnumConverter))] + [JsonConverter(typeof(JsonStringEnumConverter))] public CapacityState State { get; set; } } \ No newline at end of file diff --git a/DataFactory.MCP.Core/Models/Capacity/CapacityState.cs b/DataFactory.MCP.Core/Models/Capacity/CapacityState.cs index cb56b5e8..a9cea24c 100644 --- a/DataFactory.MCP.Core/Models/Capacity/CapacityState.cs +++ b/DataFactory.MCP.Core/Models/Capacity/CapacityState.cs @@ -5,7 +5,7 @@ namespace DataFactory.MCP.Models.Capacity; /// /// A capacity state. Additional capacity states may be added over time. /// -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum CapacityState { /// diff --git a/DataFactory.MCP.Core/Models/Common/EmptyRequest.cs b/DataFactory.MCP.Core/Models/Common/EmptyRequest.cs new file mode 100644 index 00000000..e33cb94a --- /dev/null +++ b/DataFactory.MCP.Core/Models/Common/EmptyRequest.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace DataFactory.MCP.Models.Common; + +/// +/// Empty request body for API calls that require a POST with empty JSON payload. +/// Used instead of anonymous types to support source-gen JSON serialization. +/// +public class EmptyRequest { } diff --git a/DataFactory.MCP.Core/Models/Common/RunOnDemandRequest.cs b/DataFactory.MCP.Core/Models/Common/RunOnDemandRequest.cs new file mode 100644 index 00000000..43fa6c2d --- /dev/null +++ b/DataFactory.MCP.Core/Models/Common/RunOnDemandRequest.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace DataFactory.MCP.Models.Common; + +/// +/// Request payload for running an item job on demand with optional execution data. +/// Used by both Pipeline and CopyJob run operations. +/// +public class RunOnDemandRequest +{ + /// + /// Optional execution data for the job run + /// + [JsonPropertyName("executionData")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public JsonElement? ExecutionData { get; set; } +} diff --git a/DataFactory.MCP.Core/Models/Connection/Connection.cs b/DataFactory.MCP.Core/Models/Connection/Connection.cs index 427e1eb9..33ee813f 100644 --- a/DataFactory.MCP.Core/Models/Connection/Connection.cs +++ b/DataFactory.MCP.Core/Models/Connection/Connection.cs @@ -5,6 +5,7 @@ namespace DataFactory.MCP.Models.Connection; /// /// Base connection class /// +[JsonConverter(typeof(ConnectionJsonConverter))] public abstract class Connection { [JsonPropertyName("id")] diff --git a/DataFactory.MCP.Core/Models/Connection/ConnectionEnums.cs b/DataFactory.MCP.Core/Models/Connection/ConnectionEnums.cs index d52fc5ad..99f5682e 100644 --- a/DataFactory.MCP.Core/Models/Connection/ConnectionEnums.cs +++ b/DataFactory.MCP.Core/Models/Connection/ConnectionEnums.cs @@ -5,7 +5,7 @@ namespace DataFactory.MCP.Models.Connection; /// /// The connectivity type of the connection /// -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum ConnectivityType { ShareableCloud, @@ -20,7 +20,7 @@ public enum ConnectivityType /// /// The credential type of the connection /// -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum CredentialType { Windows, @@ -37,7 +37,7 @@ public enum CredentialType /// /// The connection encryption type of the connection /// -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum ConnectionEncryption { Encrypted, @@ -48,7 +48,7 @@ public enum ConnectionEncryption /// /// The privacy level setting of the connection /// -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum PrivacyLevel { None, @@ -60,7 +60,7 @@ public enum PrivacyLevel /// /// The single sign-on type of the connection /// -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum SingleSignOnType { None, diff --git a/DataFactory.MCP.Core/Models/Connection/ConnectionJsonConverter.cs b/DataFactory.MCP.Core/Models/Connection/ConnectionJsonConverter.cs index ca52b4f4..ea4d4915 100644 --- a/DataFactory.MCP.Core/Models/Connection/ConnectionJsonConverter.cs +++ b/DataFactory.MCP.Core/Models/Connection/ConnectionJsonConverter.cs @@ -1,10 +1,12 @@ using System.Text.Json; using System.Text.Json.Serialization; +using DataFactory.MCP.Configuration; namespace DataFactory.MCP.Models.Connection; /// -/// JSON converter for handling polymorphic Connection types based on ConnectivityType +/// JSON converter for handling polymorphic Connection types based on ConnectivityType. +/// Uses source-generated JsonTypeInfo for trim-safe serialization. /// public class ConnectionJsonConverter : JsonConverter { @@ -25,24 +27,39 @@ public class ConnectionJsonConverter : JsonConverter } var json = root.GetRawText(); - var jsonOptions = new JsonSerializerOptions(options); - jsonOptions.Converters.Remove(this); // Prevent infinite recursion return connectivityType switch { - ConnectivityType.ShareableCloud => JsonSerializer.Deserialize(json, jsonOptions), - ConnectivityType.PersonalCloud => JsonSerializer.Deserialize(json, jsonOptions), - ConnectivityType.OnPremisesGateway => JsonSerializer.Deserialize(json, jsonOptions), - ConnectivityType.OnPremisesGatewayPersonal => JsonSerializer.Deserialize(json, jsonOptions), - ConnectivityType.VirtualNetworkGateway => JsonSerializer.Deserialize(json, jsonOptions), + ConnectivityType.ShareableCloud => JsonSerializer.Deserialize(json, DataFactoryJsonContext.Default.ShareableCloudConnection), + ConnectivityType.PersonalCloud => JsonSerializer.Deserialize(json, DataFactoryJsonContext.Default.PersonalCloudConnection), + ConnectivityType.OnPremisesGateway => JsonSerializer.Deserialize(json, DataFactoryJsonContext.Default.OnPremisesGatewayConnection), + ConnectivityType.OnPremisesGatewayPersonal => JsonSerializer.Deserialize(json, DataFactoryJsonContext.Default.OnPremisesGatewayPersonalConnection), + ConnectivityType.VirtualNetworkGateway => JsonSerializer.Deserialize(json, DataFactoryJsonContext.Default.VirtualNetworkGatewayConnection), _ => throw new JsonException($"Unsupported connectivity type: {connectivityType}") }; } public override void Write(Utf8JsonWriter writer, Connection value, JsonSerializerOptions options) { - var jsonOptions = new JsonSerializerOptions(options); - jsonOptions.Converters.Remove(this); // Prevent infinite recursion - JsonSerializer.Serialize(writer, value, value.GetType(), jsonOptions); + switch (value) + { + case ShareableCloudConnection c: + JsonSerializer.Serialize(writer, c, DataFactoryJsonContext.Default.ShareableCloudConnection); + break; + case PersonalCloudConnection c: + JsonSerializer.Serialize(writer, c, DataFactoryJsonContext.Default.PersonalCloudConnection); + break; + case OnPremisesGatewayConnection c: + JsonSerializer.Serialize(writer, c, DataFactoryJsonContext.Default.OnPremisesGatewayConnection); + break; + case OnPremisesGatewayPersonalConnection c: + JsonSerializer.Serialize(writer, c, DataFactoryJsonContext.Default.OnPremisesGatewayPersonalConnection); + break; + case VirtualNetworkGatewayConnection c: + JsonSerializer.Serialize(writer, c, DataFactoryJsonContext.Default.VirtualNetworkGatewayConnection); + break; + default: + throw new JsonException($"Unsupported connection type: {value.GetType()}"); + } } } \ No newline at end of file diff --git a/DataFactory.MCP.Core/Models/Gateway/GatewayJsonConverter.cs b/DataFactory.MCP.Core/Models/Gateway/GatewayJsonConverter.cs index 7e1ee756..9fc14add 100644 --- a/DataFactory.MCP.Core/Models/Gateway/GatewayJsonConverter.cs +++ b/DataFactory.MCP.Core/Models/Gateway/GatewayJsonConverter.cs @@ -1,10 +1,12 @@ using System.Text.Json; using System.Text.Json.Serialization; +using DataFactory.MCP.Configuration; namespace DataFactory.MCP.Models.Gateway; /// -/// Custom JSON converter for Gateway polymorphic deserialization +/// Custom JSON converter for Gateway polymorphic deserialization. +/// Uses source-generated JsonTypeInfo for trim-safe serialization. /// public class GatewayJsonConverter : JsonConverter { @@ -13,7 +15,6 @@ public override Gateway Read(ref Utf8JsonReader reader, Type typeToConvert, Json using JsonDocument doc = JsonDocument.ParseValue(ref reader); JsonElement root = doc.RootElement; - // Get the type property to determine which concrete type to deserialize to if (!root.TryGetProperty("type", out JsonElement typeElement)) { throw new JsonException("Gateway object must have a 'type' property"); @@ -21,18 +22,30 @@ public override Gateway Read(ref Utf8JsonReader reader, Type typeToConvert, Json string gatewayType = typeElement.GetString() ?? string.Empty; - // Deserialize to the appropriate concrete type based on the type field return gatewayType switch { - "OnPremises" => JsonSerializer.Deserialize(root.GetRawText(), options)!, - "OnPremisesPersonal" => JsonSerializer.Deserialize(root.GetRawText(), options)!, - "VirtualNetwork" => JsonSerializer.Deserialize(root.GetRawText(), options)!, + "OnPremises" => JsonSerializer.Deserialize(root.GetRawText(), DataFactoryJsonContext.Default.OnPremisesGateway)!, + "OnPremisesPersonal" => JsonSerializer.Deserialize(root.GetRawText(), DataFactoryJsonContext.Default.OnPremisesGatewayPersonal)!, + "VirtualNetwork" => JsonSerializer.Deserialize(root.GetRawText(), DataFactoryJsonContext.Default.VirtualNetworkGateway)!, _ => throw new JsonException($"Unknown gateway type: {gatewayType}") }; } public override void Write(Utf8JsonWriter writer, Gateway value, JsonSerializerOptions options) { - JsonSerializer.Serialize(writer, value, value.GetType(), options); + switch (value) + { + case OnPremisesGateway g: + JsonSerializer.Serialize(writer, g, DataFactoryJsonContext.Default.OnPremisesGateway); + break; + case OnPremisesGatewayPersonal g: + JsonSerializer.Serialize(writer, g, DataFactoryJsonContext.Default.OnPremisesGatewayPersonal); + break; + case VirtualNetworkGateway g: + JsonSerializer.Serialize(writer, g, DataFactoryJsonContext.Default.VirtualNetworkGateway); + break; + default: + throw new JsonException($"Unsupported gateway type: {value.GetType()}"); + } } } diff --git a/DataFactory.MCP.Core/Models/Pipeline/Schedule/CreateScheduleRequest.cs b/DataFactory.MCP.Core/Models/Pipeline/Schedule/CreateScheduleRequest.cs index 3c81a182..5d8bba3e 100644 --- a/DataFactory.MCP.Core/Models/Pipeline/Schedule/CreateScheduleRequest.cs +++ b/DataFactory.MCP.Core/Models/Pipeline/Schedule/CreateScheduleRequest.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using System.Text.Json.Serialization; namespace DataFactory.MCP.Models.Pipeline.Schedule; @@ -17,5 +18,5 @@ public class CreateScheduleRequest /// The schedule configuration (Cron, Daily, Weekly, or Monthly) /// [JsonPropertyName("configuration")] - public object Configuration { get; set; } = null!; + public JsonElement Configuration { get; set; } } diff --git a/DataFactory.MCP.Core/Models/Workspace/WorkspaceType.cs b/DataFactory.MCP.Core/Models/Workspace/WorkspaceType.cs index 204bae87..148062b4 100644 --- a/DataFactory.MCP.Core/Models/Workspace/WorkspaceType.cs +++ b/DataFactory.MCP.Core/Models/Workspace/WorkspaceType.cs @@ -5,7 +5,7 @@ namespace DataFactory.MCP.Models.Workspace; /// /// A workspace type. Additional workspace types may be added over time. /// -[JsonConverter(typeof(JsonStringEnumConverter))] +[JsonConverter(typeof(JsonStringEnumConverter))] public enum WorkspaceType { /// diff --git a/DataFactory.MCP.Core/Resources/McpApps/package-lock.json b/DataFactory.MCP.Core/Resources/McpApps/package-lock.json index 3df32d99..7fa9fa87 100644 --- a/DataFactory.MCP.Core/Resources/McpApps/package-lock.json +++ b/DataFactory.MCP.Core/Resources/McpApps/package-lock.json @@ -8,7 +8,7 @@ "name": "datafactory-mcp-ui-apps", "version": "1.0.0", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.1", + "@modelcontextprotocol/ext-apps": "^1.7.1", "@modelcontextprotocol/sdk": "^1.26.0", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -18,7 +18,7 @@ "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.2.0", "typescript": "^5.3.0", - "vite": "^7.3.1", + "vite": "^7.3.2", "vite-plugin-singlefile": "^2.0.0" } }, @@ -747,9 +747,9 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.9", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", "license": "MIT", "engines": { "node": ">=18.14.1" @@ -809,35 +809,21 @@ } }, "node_modules/@modelcontextprotocol/ext-apps": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.0.1.tgz", - "integrity": "sha512-rAPzBbB5GNgYk216paQjGKUgbNXSy/yeR95c0ni6Y4uvhWI2AeF+ztEOqQFLBMQy/MPM+02pbVK1HaQmQjMwYQ==", - "hasInstallScript": true, + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.1.tgz", + "integrity": "sha512-J3WdG1A4JSSKnSWKyU+895dBVYBV2Utgtf7fUsUK45mlkETm53a/1DR6Pm3hUGKqLLQthZLmpxOg8VPzJi/lyg==", "license": "MIT", "workspaces": [ "examples/*" ], - "optionalDependencies": { - "@oven/bun-darwin-aarch64": "^1.2.21", - "@oven/bun-darwin-x64": "^1.2.21", - "@oven/bun-darwin-x64-baseline": "^1.2.21", - "@oven/bun-linux-aarch64": "^1.2.21", - "@oven/bun-linux-aarch64-musl": "^1.2.21", - "@oven/bun-linux-x64": "^1.2.21", - "@oven/bun-linux-x64-baseline": "^1.2.21", - "@oven/bun-linux-x64-musl": "^1.2.21", - "@oven/bun-linux-x64-musl-baseline": "^1.2.21", - "@oven/bun-windows-x64": "^1.2.21", - "@oven/bun-windows-x64-baseline": "^1.2.21", - "@rollup/rollup-darwin-arm64": "^4.53.3", - "@rollup/rollup-darwin-x64": "^4.53.3", - "@rollup/rollup-linux-arm64-gnu": "^4.53.3", - "@rollup/rollup-linux-x64-gnu": "^4.53.3", - "@rollup/rollup-win32-arm64-msvc": "^4.53.3", - "@rollup/rollup-win32-x64-msvc": "^4.53.3" + "dependencies": { + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">=20" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/sdk": "^1.29.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^3.25.0 || ^4.0.0" @@ -852,9 +838,9 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", - "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", "dependencies": { "@hono/node-server": "^1.19.9", @@ -891,149 +877,6 @@ } } }, - "node_modules/@oven/bun-darwin-aarch64": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-aarch64/-/bun-darwin-aarch64-1.3.9.tgz", - "integrity": "sha512-df7smckMWSUfaT5mzwN9Lfpd3ZGkOqo+vmQ8VV2a32gl14v6uZ/qeeo+1RlANXn8M0uzXPWWCkrKZIWSZUR0qw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oven/bun-darwin-x64": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64/-/bun-darwin-x64-1.3.9.tgz", - "integrity": "sha512-YiLxfsPzQqaVvT2a+nxH9do0YfUjrlxF3tKP0b1DDgvfgCcVKGsrQH3Wa82qHgL4dnT8h2bqi94JxXESEuPmcA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oven/bun-darwin-x64-baseline": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@oven/bun-darwin-x64-baseline/-/bun-darwin-x64-baseline-1.3.9.tgz", - "integrity": "sha512-XbhsA2XAFzvFr0vPSV6SNqGxab4xHKdPmVTLqoSHAx9tffrSq/012BDptOskulwnD+YNsrJUx2D2Ve1xvfgGcg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@oven/bun-linux-aarch64": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64/-/bun-linux-aarch64-1.3.9.tgz", - "integrity": "sha512-VaNQTu0Up4gnwZLQ6/Hmho6jAlLxTQ1PwxEth8EsXHf82FOXXPV5OCQ6KC9mmmocjKlmWFaIGebThrOy8DUo4g==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oven/bun-linux-aarch64-musl": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-aarch64-musl/-/bun-linux-aarch64-musl-1.3.9.tgz", - "integrity": "sha512-t8uimCVBTw5f9K2QTZE5wN6UOrFETNrh/Xr7qtXT9nAOzaOnIFvYA+HcHbGfi31fRlCVfTxqm/EiCwJ1gEw9YQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oven/bun-linux-x64": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64/-/bun-linux-x64-1.3.9.tgz", - "integrity": "sha512-oQyAW3+ugulvXTZ+XYeUMmNPR94sJeMokfHQoKwPvVwhVkgRuMhcLGV2ZesHCADVu30Oz2MFXbgdC8x4/o9dRg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oven/bun-linux-x64-baseline": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-baseline/-/bun-linux-x64-baseline-1.3.9.tgz", - "integrity": "sha512-nZ12g22cy7pEOBwAxz2tp0wVqekaCn9QRKuGTHqOdLlyAqR4SCdErDvDhUWd51bIyHTQoCmj72TegGTgG0WNPw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oven/bun-linux-x64-musl": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl/-/bun-linux-x64-musl-1.3.9.tgz", - "integrity": "sha512-4ZjIUgCxEyKwcKXideB5sX0KJpnHTZtu778w73VNq2uNH2fNpMZv98+DBgJyQ9OfFoRhmKn1bmLmSefvnHzI9w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oven/bun-linux-x64-musl-baseline": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@oven/bun-linux-x64-musl-baseline/-/bun-linux-x64-musl-baseline-1.3.9.tgz", - "integrity": "sha512-3FXQgtYFsT0YOmAdMcJn56pLM5kzSl6y942rJJIl5l2KummB9Ea3J/vMJMzQk7NCAGhleZGWU/pJSS/uXKGa7w==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@oven/bun-windows-x64": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64/-/bun-windows-x64-1.3.9.tgz", - "integrity": "sha512-/d6vAmgKvkoYlsGPsRPlPmOK1slPis/F40UG02pYwypTH0wmY0smgzdFqR4YmryxFh17XrW1kITv+U99Oajk9Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@oven/bun-windows-x64-baseline": { - "version": "1.3.9", - "resolved": "https://registry.npmjs.org/@oven/bun-windows-x64-baseline/-/bun-windows-x64-baseline-1.3.9.tgz", - "integrity": "sha512-a/+hSrrDpMD7THyXvE2KJy1skxzAD0cnW4K1WjuI/91VqsphjNzvf5t/ZgxEVL4wb6f+hKrSJ5J3aH47zPr61g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.27", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", @@ -1042,9 +885,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", "cpu": [ "arm" ], @@ -1056,9 +899,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", "cpu": [ "arm64" ], @@ -1070,12 +913,13 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1083,12 +927,13 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1096,9 +941,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", "cpu": [ "arm64" ], @@ -1110,9 +955,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", "cpu": [ "x64" ], @@ -1124,9 +969,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", "cpu": [ "arm" ], @@ -1138,9 +983,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", "cpu": [ "arm" ], @@ -1152,12 +997,13 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1165,9 +1011,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", "cpu": [ "arm64" ], @@ -1179,9 +1025,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", "cpu": [ "loong64" ], @@ -1193,9 +1039,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", "cpu": [ "loong64" ], @@ -1207,9 +1053,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", "cpu": [ "ppc64" ], @@ -1221,9 +1067,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", "cpu": [ "ppc64" ], @@ -1235,9 +1081,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", "cpu": [ "riscv64" ], @@ -1249,9 +1095,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", "cpu": [ "riscv64" ], @@ -1263,9 +1109,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", "cpu": [ "s390x" ], @@ -1277,12 +1123,13 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1290,9 +1137,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", "cpu": [ "x64" ], @@ -1304,9 +1151,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", "cpu": [ "x64" ], @@ -1318,9 +1165,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", "cpu": [ "arm64" ], @@ -1332,12 +1179,13 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1345,9 +1193,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", "cpu": [ "ia32" ], @@ -1359,9 +1207,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", "cpu": [ "x64" ], @@ -1373,18 +1221,25 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1981,12 +1836,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", - "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", "license": "MIT", "dependencies": { - "ip-address": "10.0.1" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -2180,9 +2035,9 @@ } }, "node_modules/hono": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.0.tgz", - "integrity": "sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA==", + "version": "4.12.18", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", + "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -2231,9 +2086,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" @@ -2516,9 +2371,9 @@ } }, "node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -2533,9 +2388,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -2555,9 +2410,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -2680,9 +2535,9 @@ } }, "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", "dev": true, "license": "MIT", "dependencies": { @@ -2696,31 +2551,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", "fsevents": "~2.3.2" } }, @@ -2964,9 +2819,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -3076,9 +2931,9 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", + "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, "license": "MIT", "dependencies": { @@ -3186,9 +3041,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { diff --git a/DataFactory.MCP.Core/Resources/McpApps/package.json b/DataFactory.MCP.Core/Resources/McpApps/package.json index 2fbce461..34c59f57 100644 --- a/DataFactory.MCP.Core/Resources/McpApps/package.json +++ b/DataFactory.MCP.Core/Resources/McpApps/package.json @@ -8,7 +8,7 @@ "dev": "vite build --watch" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.0.1", + "@modelcontextprotocol/ext-apps": "^1.7.1", "@modelcontextprotocol/sdk": "^1.26.0", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -18,7 +18,7 @@ "@types/react-dom": "^18.2.0", "@vitejs/plugin-react": "^4.2.0", "typescript": "^5.3.0", - "vite": "^7.3.1", + "vite": "^7.3.2", "vite-plugin-singlefile": "^2.0.0" } -} \ No newline at end of file +} diff --git a/DataFactory.MCP.Core/Services/Authentication/TokenCredentialAuthenticationService.cs b/DataFactory.MCP.Core/Services/Authentication/TokenCredentialAuthenticationService.cs new file mode 100644 index 00000000..5a43d7fa --- /dev/null +++ b/DataFactory.MCP.Core/Services/Authentication/TokenCredentialAuthenticationService.cs @@ -0,0 +1,71 @@ +using Azure.Core; +using DataFactory.MCP.Abstractions.Interfaces; +using DataFactory.MCP.Models; +using Microsoft.Extensions.Logging; + +namespace DataFactory.MCP.Services.Authentication; + +/// +/// An IAuthenticationService implementation that delegates token acquisition to an Azure.Core TokenCredential. +/// Used when DataFactory.MCP.Core is hosted inside a system that already provides authentication +/// (e.g., Fabric MCP Server with DefaultAzureCredential). +/// +public class TokenCredentialAuthenticationService : IAuthenticationService +{ + private readonly TokenCredential _credential; + private readonly ILogger _logger; + + public TokenCredentialAuthenticationService( + TokenCredential credential, + ILogger logger) + { + _credential = credential; + _logger = logger; + } + + public Task GetAccessTokenAsync() + { + return GetAccessTokenAsync(AzureAdConfiguration.PowerBIScopes); + } + + public async Task GetAccessTokenAsync(string[] scopes) + { + var context = new TokenRequestContext(scopes); + var token = await _credential.GetTokenAsync(context, CancellationToken.None).ConfigureAwait(false); + return token.Token; + } + + public string GetAuthenticationStatus() + { + return "Authenticated via host-provided credential (TokenCredential)"; + } + + public Task AuthenticateInteractiveAsync() + { + _logger.LogInformation("Interactive authentication is not required — using host-provided credential"); + return Task.FromResult("Already authenticated via host-provided credential. No login required."); + } + + public Task StartDeviceCodeAuthAsync() + { + _logger.LogInformation("Device code authentication is not required — using host-provided credential"); + return Task.FromResult("Already authenticated via host-provided credential. No device code required."); + } + + public Task CheckDeviceAuthStatusAsync() + { + return Task.FromResult("Already authenticated via host-provided credential."); + } + + public Task AuthenticateServicePrincipalAsync(string applicationId, string clientSecret, string? tenantId = null) + { + _logger.LogInformation("Service principal authentication is not required — using host-provided credential"); + return Task.FromResult("Already authenticated via host-provided credential. No service principal login required."); + } + + public Task SignOutAsync() + { + _logger.LogInformation("Sign out is not applicable — authentication is managed by the host"); + return Task.FromResult("Authentication is managed by the host. Sign out is not applicable."); + } +} diff --git a/DataFactory.MCP.Core/Services/BackgroundTasks/Jobs/DataflowRefreshJob.cs b/DataFactory.MCP.Core/Services/BackgroundTasks/Jobs/DataflowRefreshJob.cs index 4584a546..d08ccde7 100644 --- a/DataFactory.MCP.Core/Services/BackgroundTasks/Jobs/DataflowRefreshJob.cs +++ b/DataFactory.MCP.Core/Services/BackgroundTasks/Jobs/DataflowRefreshJob.cs @@ -1,4 +1,4 @@ -using System.Net.Http.Json; +using System.Text.Json; using DataFactory.MCP.Abstractions.Interfaces; using DataFactory.MCP.Configuration; using DataFactory.MCP.Infrastructure.Http; @@ -68,8 +68,8 @@ public async Task StartAsync() }; } - var jsonContent = System.Text.Json.JsonSerializer.Serialize(request, - JsonSerializerOptionsProvider.FabricApi); + var jsonContent = JsonSerializer.Serialize(request, + typeof(RunOnDemandExecuteRequest), DataFactoryJsonContext.Default); var content = new StringContent(jsonContent, System.Text.Encoding.UTF8, "application/json"); _logger.LogInformation("Starting dataflow refresh: POST {Url}", url); @@ -153,8 +153,9 @@ public async Task CheckStatusAsync() var response = await httpClient.GetAsync(url); response.EnsureSuccessStatusCode(); - var jobInstance = await response.Content.ReadFromJsonAsync( - JsonSerializerOptionsProvider.FabricApi); + var content = await response.Content.ReadAsStringAsync(); + var jobInstance = (ItemJobInstance?)JsonSerializer.Deserialize( + content, typeof(ItemJobInstance), DataFactoryJsonContext.Default); if (jobInstance == null) { diff --git a/DataFactory.MCP.Core/Services/DMTSv2/GatewayClusterDatasourceService.cs b/DataFactory.MCP.Core/Services/DMTSv2/GatewayClusterDatasourceService.cs index 52cf917c..9f83563b 100644 --- a/DataFactory.MCP.Core/Services/DMTSv2/GatewayClusterDatasourceService.cs +++ b/DataFactory.MCP.Core/Services/DMTSv2/GatewayClusterDatasourceService.cs @@ -79,7 +79,7 @@ private async Task> GetCloudDatasourcesAsync() _logger.LogDebug("Fetching cloud datasources from Power BI v2.0 API"); var response = await _httpClient.GetAsync(GatewayClusterDatasourcesUrl); - var result = await response.ReadAsJsonAsync(JsonSerializerOptionsProvider.FabricApi); + var result = await response.ReadAsJsonAsync(); var datasources = result?.Value ?? new List(); @@ -97,7 +97,7 @@ private async Task> GetCloudDatasourcesAsync() /// /// Minimal model for cloud datasource info from the v2 API /// - private sealed class CloudDatasourceInfo + internal sealed class CloudDatasourceInfo { [JsonPropertyName("id")] public string Id { get; set; } = string.Empty; @@ -109,7 +109,7 @@ private sealed class CloudDatasourceInfo /// /// Response wrapper for the gatewayClusterDatasources API /// - private sealed class GatewayClusterDatasourcesResponse + internal sealed class GatewayClusterDatasourcesResponse { [JsonPropertyName("value")] public List Value { get; set; } = new(); diff --git a/DataFactory.MCP.Core/Services/FabricCopyJobService.cs b/DataFactory.MCP.Core/Services/FabricCopyJobService.cs index 26885281..f4861aa7 100644 --- a/DataFactory.MCP.Core/Services/FabricCopyJobService.cs +++ b/DataFactory.MCP.Core/Services/FabricCopyJobService.cs @@ -1,6 +1,8 @@ +using System.Text.Json; using DataFactory.MCP.Abstractions; using DataFactory.MCP.Abstractions.Interfaces; using DataFactory.MCP.Infrastructure.Http; +using DataFactory.MCP.Models.Common; using DataFactory.MCP.Models.CopyJob; using DataFactory.MCP.Models.CopyJob.Definition; using DataFactory.MCP.Models.Pipeline; @@ -123,7 +125,7 @@ public async Task GetCopyJobDefinitionAsync( Logger.LogInformation("Getting definition for copy job {CopyJobId} in workspace {WorkspaceId}", copyJobId, workspaceId); - var emptyRequest = new { }; + var emptyRequest = new EmptyRequest(); var response = await PostAsync(endpoint, emptyRequest) ?? throw new InvalidOperationException("Failed to get copy job definition response"); @@ -199,7 +201,7 @@ public async Task UpdateCopyJobDefinitionAsync( public async Task RunCopyJobAsync( string workspaceId, string copyJobId, - object? executionData = null) + JsonElement? executionData = null) { try { @@ -213,7 +215,9 @@ public async Task UpdateCopyJobDefinitionAsync( Logger.LogInformation("Running copy job {CopyJobId} on demand in workspace {WorkspaceId}", copyJobId, workspaceId); - var request = executionData != null ? new { executionData } : null; + var request = executionData != null + ? new RunOnDemandRequest { ExecutionData = executionData } + : null; var location = await PostAndGetLocationAsync(endpoint, request); Logger.LogInformation("Copy job {CopyJobId} run triggered successfully. Location: {Location}", diff --git a/DataFactory.MCP.Core/Services/FabricDataflowService.cs b/DataFactory.MCP.Core/Services/FabricDataflowService.cs index d29febd4..2479dbf9 100644 --- a/DataFactory.MCP.Core/Services/FabricDataflowService.cs +++ b/DataFactory.MCP.Core/Services/FabricDataflowService.cs @@ -2,6 +2,7 @@ using DataFactory.MCP.Abstractions.Interfaces; using DataFactory.MCP.Abstractions.Interfaces.DMTSv2; using DataFactory.MCP.Infrastructure.Http; +using DataFactory.MCP.Models.Common; using DataFactory.MCP.Models.Dataflow; using DataFactory.MCP.Models.Dataflow.Definition; using DataFactory.MCP.Models.Dataflow.Query; @@ -194,7 +195,7 @@ private async Task GetDataflowDefinitionRespo dataflowId, workspaceId); // Use empty object as required by API - var emptyRequest = new { }; + var emptyRequest = new EmptyRequest(); return await PostAsync(endpoint, emptyRequest) ?? throw new InvalidOperationException("Failed to get dataflow definition response"); } diff --git a/DataFactory.MCP.Core/Services/FabricPipelineService.cs b/DataFactory.MCP.Core/Services/FabricPipelineService.cs index 0993f7bc..8732de66 100644 --- a/DataFactory.MCP.Core/Services/FabricPipelineService.cs +++ b/DataFactory.MCP.Core/Services/FabricPipelineService.cs @@ -1,6 +1,8 @@ +using System.Text.Json; using DataFactory.MCP.Abstractions; using DataFactory.MCP.Abstractions.Interfaces; using DataFactory.MCP.Infrastructure.Http; +using DataFactory.MCP.Models.Common; using DataFactory.MCP.Models.Pipeline; using DataFactory.MCP.Models.Pipeline.Definition; using DataFactory.MCP.Models.Pipeline.Schedule; @@ -122,7 +124,7 @@ public async Task GetPipelineDefinitionAsync( Logger.LogInformation("Getting definition for pipeline {PipelineId} in workspace {WorkspaceId}", pipelineId, workspaceId); - var emptyRequest = new { }; + var emptyRequest = new EmptyRequest(); var response = await PostAsync(endpoint, emptyRequest) ?? throw new InvalidOperationException("Failed to get pipeline definition response"); @@ -198,7 +200,7 @@ public async Task UpdatePipelineDefinitionAsync( public async Task RunPipelineAsync( string workspaceId, string pipelineId, - object? executionData = null) + JsonElement? executionData = null) { try { @@ -212,7 +214,9 @@ public async Task UpdatePipelineDefinitionAsync( Logger.LogInformation("Running pipeline {PipelineId} on demand in workspace {WorkspaceId}", pipelineId, workspaceId); - var request = executionData != null ? new { executionData } : null; + var request = executionData != null + ? new RunOnDemandRequest { ExecutionData = executionData } + : null; var location = await PostAndGetLocationAsync(endpoint, request); Logger.LogInformation("Pipeline {PipelineId} run triggered successfully. Location: {Location}", diff --git a/DataFactory.MCP.Core/Services/ValidationService.cs b/DataFactory.MCP.Core/Services/ValidationService.cs index 069352c8..95e64f7c 100644 --- a/DataFactory.MCP.Core/Services/ValidationService.cs +++ b/DataFactory.MCP.Core/Services/ValidationService.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; using DataFactory.MCP.Abstractions.Interfaces; using DataFactory.MCP.Models; @@ -9,6 +10,7 @@ namespace DataFactory.MCP.Services; /// public class ValidationService : IValidationService { + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Validation types are preserved by the application")] public void ValidateAndThrow(T obj, string parameterName) where T : class { if (obj == null) @@ -26,6 +28,7 @@ public void ValidateAndThrow(T obj, string parameterName) where T : class } } + [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "Validation types are preserved by the application")] public IList Validate(T obj) where T : class { if (obj == null) diff --git a/DataFactory.MCP.Core/Tools/CopyJob/CopyJobTool.cs b/DataFactory.MCP.Core/Tools/CopyJob/CopyJobTool.cs index 80d5db64..e8c94e4f 100644 --- a/DataFactory.MCP.Core/Tools/CopyJob/CopyJobTool.cs +++ b/DataFactory.MCP.Core/Tools/CopyJob/CopyJobTool.cs @@ -334,12 +334,12 @@ public async Task RunCopyJobAsync( _validationService.ValidateRequiredString(workspaceId, nameof(workspaceId)); _validationService.ValidateRequiredString(copyJobId, nameof(copyJobId)); - object? executionData = null; + JsonElement? executionData = null; if (!string.IsNullOrEmpty(executionDataJson)) { try { - executionData = JsonSerializer.Deserialize(executionDataJson); + executionData = JsonSerializer.Deserialize(executionDataJson); } catch (JsonException ex) { @@ -453,11 +453,10 @@ public async Task CreateCopyJobScheduleAsync( _validationService.ValidateRequiredString(copyJobId, nameof(copyJobId)); _validationService.ValidateRequiredString(configurationJson, nameof(configurationJson)); - object configuration; + JsonElement configuration; try { - configuration = JsonSerializer.Deserialize(configurationJson) - ?? throw new ArgumentException("Configuration JSON cannot be null"); + configuration = JsonSerializer.Deserialize(configurationJson); } catch (JsonException ex) { diff --git a/DataFactory.MCP.Core/Tools/Dataflow/DataflowQueryTool.cs b/DataFactory.MCP.Core/Tools/Dataflow/DataflowQueryTool.cs index fc080d76..fb176da5 100644 --- a/DataFactory.MCP.Core/Tools/Dataflow/DataflowQueryTool.cs +++ b/DataFactory.MCP.Core/Tools/Dataflow/DataflowQueryTool.cs @@ -1,8 +1,8 @@ using ModelContextProtocol.Server; using System.ComponentModel; -using DataFactory.MCP.Abstractions.Interfaces; using DataFactory.MCP.Extensions; -using DataFactory.MCP.Models.Dataflow.Query; +using DataFactory.MCP.Handlers; +using DataFactory.MCP.Handlers.Dataflow; namespace DataFactory.MCP.Tools.Dataflow; @@ -12,13 +12,11 @@ namespace DataFactory.MCP.Tools.Dataflow; [McpServerToolType] public class DataflowQueryTool { - private readonly IFabricDataflowService _dataflowService; - private readonly IValidationService _validationService; + private readonly DataflowQueryHandler _handler; - public DataflowQueryTool(IFabricDataflowService dataflowService, IValidationService validationService) + public DataflowQueryTool(DataflowQueryHandler handler) { - _dataflowService = dataflowService ?? throw new ArgumentNullException(nameof(dataflowService)); - _validationService = validationService ?? throw new ArgumentNullException(nameof(validationService)); + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); } [McpServerTool, Description(@"Executes a query against a dataflow and returns the complete results (all data) in Apache Arrow format. This allows you to run M (Power Query) language queries against data sources connected through the dataflow and get the full dataset. @@ -30,48 +28,11 @@ public async Task ExecuteQueryAsync( [Description("The name of the query to execute (required)")] string queryName, [Description("The M (Power Query) language query to execute. Can be either a raw M expression (which will be auto-wrapped) or a complete section document. Results will be returned as structured data - format the table.rows as a markdown table for user display.")] string customMashupDocument) { - try - { - // Validate required parameters using validation service - _validationService.ValidateRequiredString(workspaceId, nameof(workspaceId)); - _validationService.ValidateRequiredString(dataflowId, nameof(dataflowId)); - _validationService.ValidateRequiredString(queryName, nameof(queryName)); - _validationService.ValidateRequiredString(customMashupDocument, nameof(customMashupDocument)); + var result = await _handler.ExecuteQueryAsync(workspaceId, dataflowId, queryName, customMashupDocument); - // Auto-wrap the query if it's not already in section format - var wrappedQuery = customMashupDocument.WrapForDataflowQuery(queryName); + if (result.IsSuccess) + return result.Value!.Data!.ToMcpJson(); - // Execute the query - var request = new ExecuteDataflowQueryRequest - { - QueryName = queryName, - CustomMashupDocument = wrappedQuery - }; - - var response = await _dataflowService.ExecuteQueryAsync(workspaceId, dataflowId, request); - - // Return formatted response - var result = response.Success - ? response.CreateArrowDataReport() - : response.ToQueryExecutionError(workspaceId, dataflowId, queryName); - - return result.ToMcpJson(); - } - catch (ArgumentException ex) - { - return ex.ToValidationError().ToMcpJson(); - } - catch (UnauthorizedAccessException ex) - { - return ex.ToAuthenticationError().ToMcpJson(); - } - catch (HttpRequestException ex) - { - return ex.ToHttpError().ToMcpJson(); - } - catch (Exception ex) - { - return ex.ToOperationError("executing dataflow query").ToMcpJson(); - } + return result.ToErrorResponse("executing dataflow query").ToMcpJson(); } } diff --git a/DataFactory.MCP.Core/Tools/Dataflow/DataflowTool.cs b/DataFactory.MCP.Core/Tools/Dataflow/DataflowTool.cs index 1640b107..5a32f419 100644 --- a/DataFactory.MCP.Core/Tools/Dataflow/DataflowTool.cs +++ b/DataFactory.MCP.Core/Tools/Dataflow/DataflowTool.cs @@ -2,6 +2,8 @@ using System.ComponentModel; using DataFactory.MCP.Abstractions.Interfaces; using DataFactory.MCP.Extensions; +using DataFactory.MCP.Handlers; +using DataFactory.MCP.Handlers.Dataflow; using DataFactory.MCP.Models.Dataflow; namespace DataFactory.MCP.Tools.Dataflow; @@ -16,15 +18,18 @@ public class DataflowTool private readonly IFabricDataflowService _dataflowService; private readonly IFabricConnectionService _connectionService; private readonly IValidationService _validationService; + private readonly DataflowHandler _dataflowHandler; public DataflowTool( IFabricDataflowService dataflowService, IFabricConnectionService connectionService, - IValidationService validationService) + IValidationService validationService, + DataflowHandler dataflowHandler) { _dataflowService = dataflowService; _connectionService = connectionService; _validationService = validationService; + _dataflowHandler = dataflowHandler; } [McpServerTool, Description(@"Returns a list of Dataflows from the specified workspace. This API supports pagination.")] @@ -32,45 +37,21 @@ public async Task ListDataflowsAsync( [Description("The workspace ID to list dataflows from (required)")] string workspaceId, [Description("A token for retrieving the next page of results (optional)")] string? continuationToken = null) { - try + var result = await _dataflowHandler.ListAsync(workspaceId, continuationToken); + if (result.IsSuccess) { - _validationService.ValidateRequiredString(workspaceId, nameof(workspaceId)); - - var response = await _dataflowService.ListDataflowsAsync(workspaceId, continuationToken); - - if (!response.Value.Any()) - { - return $"No dataflows found in workspace '{workspaceId}'."; - } - - var result = new + var value = result.Value!; + return new { - WorkspaceId = workspaceId, - DataflowCount = response.Value.Count, - ContinuationToken = response.ContinuationToken, - ContinuationUri = response.ContinuationUri, - HasMoreResults = !string.IsNullOrEmpty(response.ContinuationToken), - Dataflows = response.Value.Select(d => d.ToFormattedInfo()) - }; - - return result.ToMcpJson(); - } - catch (ArgumentException ex) - { - return ex.ToValidationError().ToMcpJson(); - } - catch (UnauthorizedAccessException ex) - { - return ex.ToAuthenticationError().ToMcpJson(); - } - catch (HttpRequestException ex) - { - return ex.ToHttpError().ToMcpJson(); - } - catch (Exception ex) - { - return ex.ToOperationError("listing dataflows").ToMcpJson(); + value.WorkspaceId, + value.DataflowCount, + value.ContinuationToken, + value.ContinuationUri, + value.HasMoreResults, + Dataflows = value.Dataflows.Select(d => d.ToFormattedInfo()) + }.ToMcpJson(); } + return result.ToErrorResponse("listing dataflows").ToMcpJson(); } [McpServerTool, Description(@"Creates a Dataflow in the specified workspace. The workspace must be on a supported Fabric capacity.")] @@ -80,18 +61,11 @@ public async Task CreateDataflowAsync( [Description("The Dataflow description (optional, max 256 characters)")] string? description = null, [Description("The folder ID where the dataflow will be created (optional, defaults to workspace root)")] string? folderId = null) { - try + var result = await _dataflowHandler.CreateAsync(workspaceId, displayName, description, folderId); + if (result.IsSuccess) { - var request = new CreateDataflowRequest - { - DisplayName = displayName, - Description = description, - FolderId = folderId - }; - - var response = await _dataflowService.CreateDataflowAsync(workspaceId, request); - - var result = new + var response = result.Value!.Dataflow; + return new { Success = true, Message = $"Dataflow '{displayName}' created successfully", @@ -102,31 +76,9 @@ public async Task CreateDataflowAsync( WorkspaceId = response.WorkspaceId, FolderId = response.FolderId, CreatedAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") - }; - - return result.ToMcpJson(); - } - catch (ArgumentException ex) - { - return ex.ToValidationError().ToMcpJson(); - } - catch (UnauthorizedAccessException ex) - { - return ex.ToAuthenticationError().ToMcpJson(); - } - catch (HttpRequestException ex) when (ex.Message.Contains("403") || ex.Message.Contains("Forbidden")) - { - return new HttpRequestException("Access denied or feature not available. The workspace must be on a supported Fabric capacity to create dataflows.") - .ToHttpError().ToMcpJson(); - } - catch (HttpRequestException ex) - { - return ex.ToHttpError().ToMcpJson(); - } - catch (Exception ex) - { - return ex.ToOperationError("creating dataflow").ToMcpJson(); + }.ToMcpJson(); } + return result.ToErrorResponse("creating dataflow").ToMcpJson(); } [McpServerTool, Description(@"Adds or replaces connections in an existing dataflow, or clears all connections. When clearExisting is true with no connectionIds, all connections are removed. When clearExisting is true with connectionIds, existing connections are replaced. When clearExisting is false (default), connections are appended.")] diff --git a/DataFactory.MCP.Core/Tools/Pipeline/PipelineTool.cs b/DataFactory.MCP.Core/Tools/Pipeline/PipelineTool.cs index fe0dccb5..dca5bc72 100644 --- a/DataFactory.MCP.Core/Tools/Pipeline/PipelineTool.cs +++ b/DataFactory.MCP.Core/Tools/Pipeline/PipelineTool.cs @@ -4,6 +4,8 @@ using System.Text.Json; using DataFactory.MCP.Abstractions.Interfaces; using DataFactory.MCP.Extensions; +using DataFactory.MCP.Handlers; +using DataFactory.MCP.Handlers.Pipeline; using DataFactory.MCP.Models.Pipeline; using DataFactory.MCP.Models.Pipeline.Definition; using DataFactory.MCP.Models.Pipeline.Schedule; @@ -19,13 +21,16 @@ public class PipelineTool { private readonly IFabricPipelineService _pipelineService; private readonly IValidationService _validationService; + private readonly PipelineHandler _pipelineHandler; public PipelineTool( IFabricPipelineService pipelineService, - IValidationService validationService) + IValidationService validationService, + PipelineHandler pipelineHandler) { _pipelineService = pipelineService; _validationService = validationService; + _pipelineHandler = pipelineHandler; } [McpServerTool, Description(@"Returns a list of Pipelines from the specified workspace. This API supports pagination.")] @@ -33,45 +38,21 @@ public async Task ListPipelinesAsync( [Description("The workspace ID to list pipelines from (required)")] string workspaceId, [Description("A token for retrieving the next page of results (optional)")] string? continuationToken = null) { - try + var result = await _pipelineHandler.ListAsync(workspaceId, continuationToken); + if (result.IsSuccess) { - _validationService.ValidateRequiredString(workspaceId, nameof(workspaceId)); - - var response = await _pipelineService.ListPipelinesAsync(workspaceId, continuationToken); - - if (!response.Value.Any()) - { - return $"No pipelines found in workspace '{workspaceId}'."; - } - - var result = new + var value = result.Value!; + return new { - WorkspaceId = workspaceId, - PipelineCount = response.Value.Count, - ContinuationToken = response.ContinuationToken, - ContinuationUri = response.ContinuationUri, - HasMoreResults = !string.IsNullOrEmpty(response.ContinuationToken), - Pipelines = response.Value.Select(p => p.ToFormattedInfo()) - }; - - return result.ToMcpJson(); - } - catch (ArgumentException ex) - { - return ex.ToValidationError().ToMcpJson(); - } - catch (UnauthorizedAccessException ex) - { - return ex.ToAuthenticationError().ToMcpJson(); - } - catch (HttpRequestException ex) - { - return ex.ToHttpError().ToMcpJson(); - } - catch (Exception ex) - { - return ex.ToOperationError("listing pipelines").ToMcpJson(); - } + value.WorkspaceId, + value.PipelineCount, + value.ContinuationToken, + value.ContinuationUri, + value.HasMoreResults, + Pipelines = value.Pipelines.Select(p => p.ToFormattedInfo()) + }.ToMcpJson(); + } + return result.ToErrorResponse("listing pipelines").ToMcpJson(); } [McpServerTool, Description(@"Creates a Pipeline in the specified workspace.")] @@ -81,18 +62,11 @@ public async Task CreatePipelineAsync( [Description("The Pipeline description (optional, max 256 characters)")] string? description = null, [Description("The folder ID where the pipeline will be created (optional, defaults to workspace root)")] string? folderId = null) { - try + var result = await _pipelineHandler.CreateAsync(workspaceId, displayName, description, folderId); + if (result.IsSuccess) { - var request = new CreatePipelineRequest - { - DisplayName = displayName, - Description = description, - FolderId = folderId - }; - - var response = await _pipelineService.CreatePipelineAsync(workspaceId, request); - - var result = new + var response = result.Value!.Pipeline; + return new { Success = true, Message = $"Pipeline '{displayName}' created successfully", @@ -103,26 +77,9 @@ public async Task CreatePipelineAsync( WorkspaceId = response.WorkspaceId, FolderId = response.FolderId, CreatedAt = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") - }; - - return result.ToMcpJson(); - } - catch (ArgumentException ex) - { - return ex.ToValidationError().ToMcpJson(); - } - catch (UnauthorizedAccessException ex) - { - return ex.ToAuthenticationError().ToMcpJson(); - } - catch (HttpRequestException ex) - { - return ex.ToHttpError().ToMcpJson(); - } - catch (Exception ex) - { - return ex.ToOperationError("creating pipeline").ToMcpJson(); + }.ToMcpJson(); } + return result.ToErrorResponse("creating pipeline").ToMcpJson(); } [McpServerTool, Description(@"Gets the metadata of a Pipeline by ID.")] @@ -130,31 +87,12 @@ public async Task GetPipelineAsync( [Description("The workspace ID containing the pipeline (required)")] string workspaceId, [Description("The pipeline ID to retrieve (required)")] string pipelineId) { - try - { - _validationService.ValidateRequiredString(workspaceId, nameof(workspaceId)); - _validationService.ValidateRequiredString(pipelineId, nameof(pipelineId)); - - var pipeline = await _pipelineService.GetPipelineAsync(workspaceId, pipelineId); - - return pipeline.ToFormattedInfo().ToMcpJson(); - } - catch (ArgumentException ex) - { - return ex.ToValidationError().ToMcpJson(); - } - catch (UnauthorizedAccessException ex) + var result = await _pipelineHandler.GetAsync(workspaceId, pipelineId); + if (result.IsSuccess) { - return ex.ToAuthenticationError().ToMcpJson(); - } - catch (HttpRequestException ex) - { - return ex.ToHttpError().ToMcpJson(); - } - catch (Exception ex) - { - return ex.ToOperationError("getting pipeline").ToMcpJson(); + return result.Value!.Pipeline.ToFormattedInfo().ToMcpJson(); } + return result.ToErrorResponse("getting pipeline").ToMcpJson(); } [McpServerTool, Description(@"Gets the definition of a Pipeline. The definition contains the pipeline JSON configuration with base64-encoded parts.")] @@ -329,63 +267,36 @@ public async Task RunPipelineAsync( [Description("The pipeline ID to run (required)")] string pipelineId, [Description("Optional execution data as JSON string for parameterized pipeline runs (optional)")] string? executionDataJson = null) { - try + JsonElement? executionData = null; + if (!string.IsNullOrEmpty(executionDataJson)) { - _validationService.ValidateRequiredString(workspaceId, nameof(workspaceId)); - _validationService.ValidateRequiredString(pipelineId, nameof(pipelineId)); - - object? executionData = null; - if (!string.IsNullOrEmpty(executionDataJson)) + try { - try - { - executionData = JsonSerializer.Deserialize(executionDataJson); - } - catch (JsonException ex) - { - throw new ArgumentException($"Invalid executionData JSON format: {ex.Message}"); - } + executionData = JsonSerializer.Deserialize(executionDataJson); } - - var location = await _pipelineService.RunPipelineAsync(workspaceId, pipelineId, executionData); - - // Extract job instance ID from the Location header URL - string? jobInstanceId = null; - if (!string.IsNullOrEmpty(location)) + catch (JsonException ex) { - var segments = new Uri(location).Segments; - jobInstanceId = segments.LastOrDefault()?.TrimEnd('/'); + return ToolResult.Failure($"Invalid executionData JSON format: {ex.Message}", "validation") + .ToErrorResponse("running pipeline").ToMcpJson(); } + } - var result = new + var result = await _pipelineHandler.RunAsync(workspaceId, pipelineId, executionData); + if (result.IsSuccess) + { + var value = result.Value!; + return new { Success = true, - Message = $"Pipeline run triggered successfully", + Message = "Pipeline run triggered successfully", PipelineId = pipelineId, WorkspaceId = workspaceId, - JobInstanceId = jobInstanceId, - LocationUrl = location, + JobInstanceId = value.JobInstanceId, + LocationUrl = value.LocationUrl, Hint = "Use GetPipelineRunStatusAsync with the jobInstanceId to check the run status" - }; - - return result.ToMcpJson(); - } - catch (ArgumentException ex) - { - return ex.ToValidationError().ToMcpJson(); - } - catch (UnauthorizedAccessException ex) - { - return ex.ToAuthenticationError().ToMcpJson(); - } - catch (HttpRequestException ex) - { - return ex.ToHttpError().ToMcpJson(); - } - catch (Exception ex) - { - return ex.ToOperationError("running pipeline").ToMcpJson(); + }.ToMcpJson(); } + return result.ToErrorResponse("running pipeline").ToMcpJson(); } [McpServerTool, Description(@"Gets the status of a Pipeline run (job instance). Use the jobInstanceId returned from RunPipelineAsync to check the run status. Possible statuses: NotStarted, InProgress, Completed, Failed, Cancelled, Deduped.")] @@ -453,11 +364,10 @@ public async Task CreatePipelineScheduleAsync( _validationService.ValidateRequiredString(pipelineId, nameof(pipelineId)); _validationService.ValidateRequiredString(configurationJson, nameof(configurationJson)); - object configuration; + JsonElement configuration; try { - configuration = JsonSerializer.Deserialize(configurationJson) - ?? throw new ArgumentException("Configuration JSON cannot be null"); + configuration = JsonSerializer.Deserialize(configurationJson); } catch (JsonException ex) { diff --git a/DataFactory.MCP.Tests/Infrastructure/McpTestFixture.cs b/DataFactory.MCP.Tests/Infrastructure/McpTestFixture.cs index 672e4585..def92a80 100644 --- a/DataFactory.MCP.Tests/Infrastructure/McpTestFixture.cs +++ b/DataFactory.MCP.Tests/Infrastructure/McpTestFixture.cs @@ -5,6 +5,8 @@ using DataFactory.MCP.Abstractions.Interfaces; using DataFactory.MCP.Abstractions.Interfaces.DMTSv2; using DataFactory.MCP.Configuration; +using DataFactory.MCP.Handlers.Dataflow; +using DataFactory.MCP.Handlers.Pipeline; using DataFactory.MCP.Infrastructure.Http; using DataFactory.MCP.Services; using DataFactory.MCP.Services.Authentication; @@ -96,6 +98,10 @@ public McpTestFixture() services.AddSingleton(); services.AddScoped(); + // Register handlers + services.AddScoped(); + services.AddScoped(); + // Register tools services.AddScoped(); services.AddScoped(); diff --git a/Directory.Build.props b/Directory.Build.props index e58806f6..fbccbd5d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ 0 - 19 + 21 0 beta diff --git a/Directory.Packages.props b/Directory.Packages.props index 0478783b..447bfd5d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,6 +3,7 @@ true + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 67690f31..4d7bffbb 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -10,6 +10,7 @@ This document provides a comprehensive overview of the Microsoft Data Factory MC - [Application Entry Point](#1-application-entry-point) - [MCP Tools Layer](#2-mcp-tools-layer) - [MCP App Resources Layer](#2a-mcp-app-resources-layer) + - [Handlers Layer](#2b-handlers-layer) - [Core Services Layer](#3-core-services-layer) - [Abstractions Layer](#4-abstractions-layer) - [Models Layer](#5-models-layer) @@ -65,6 +66,14 @@ The Microsoft Data Factory MCP Server is a .NET-based application that implement │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ Handlers Layer (new) │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ Pipeline │ │ Dataflow │ │DataflowQuery │ │ │ +│ │ │ Handler │ │ Handler │ │ Handler │ │ │ +│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Core Services Layer │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │ │ │ │ │Authentication│ │FabricGateway │ │FabricConnect │ │FabricWork- │ │ │ @@ -323,6 +332,88 @@ public class CreateConnectionResourceHandler } ``` +### 2b. Handlers Layer + +The Handlers layer sits between Tools and Services, owning all business logic for domains that need to be shared across multiple MCP framework implementations. + +#### Why Handlers Exist + +The DataFactory.MCP.Core NuGet package is consumed by two different MCP server implementations: + +1. **Standalone DataFactory MCP Server** — uses `[McpServerTool]` SDK attributes +2. **Microsoft Fabric MCP Server** ([microsoft/mcp](https://github.com/microsoft/mcp)) — uses a custom `GlobalCommand` dispatch framework + +Without handlers, every tool's business logic (validation, service calls, error handling, result formatting) would need to be duplicated in both wrappers. Handlers extract this shared logic into a single, testable location. + +#### Architecture Pattern + +``` +Tool (SDK or Fabric Command) + └── Handler (business logic) + └── Service (HTTP API calls) + └── Model (DTOs) +``` + +- **Tools** become thin delegators (~3-8 lines per method) +- **Handlers** own validation, service orchestration, result shaping, and error handling +- **Services** remain unchanged (HTTP calls, auth, response parsing) + +#### ToolResult\ + +Handlers return `ToolResult`, a framework-agnostic result type: + +```csharp +public class ToolResult +{ + public bool IsSuccess { get; } + public T? Data { get; } + public string? Error { get; } + public string? ErrorMessage { get; } +} +``` + +Each framework wrapper maps `ToolResult` to its own error format: +- SDK tools use `ToolResultExtensions` to convert errors to MCP JSON responses +- Fabric commands map `IsSuccess`/`Error` to their `CommandResult` type + +#### Current Handlers + +| Handler | Service Dependency | Operations | +|---------|-------------------|------------| +| `PipelineHandler` | `IFabricPipelineService` | List, Create, Get, Run | +| `DataflowHandler` | `IFabricDataflowService` | List, Create | +| `DataflowQueryHandler` | `IFabricDataflowService`, `IArrowDataReaderService` | Execute M Query | + +#### Example: Before and After + +**Before (tool owns business logic):** +```csharp +[McpServerTool, Description("List all pipelines")] +public async Task ListPipelinesAsync(string workspaceId) +{ + try + { + _validationService.ValidateRequiredString(workspaceId, nameof(workspaceId)); + var pipelines = await _pipelineService.ListPipelinesAsync(workspaceId); + return new { pipelines, count = pipelines.Count }.ToMcpJson(); + } + catch (ArgumentException ex) { return ex.ToValidationError().ToMcpJson(); } + catch (UnauthorizedAccessException ex) { return ex.ToAuthenticationError().ToMcpJson(); } + catch (HttpRequestException ex) { return ex.ToHttpError().ToMcpJson(); } + catch (Exception ex) { return ex.ToOperationError("list pipelines").ToMcpJson(); } +} +``` + +**After (tool delegates to handler):** +```csharp +[McpServerTool, Description("List all pipelines")] +public async Task ListPipelinesAsync(string workspaceId) +{ + var result = await _handler.ListPipelinesAsync(workspaceId); + return result.ToMcpToolResponse(); +} +``` + ### 3. Core Services Layer **Location**: `Services/` @@ -755,6 +846,10 @@ Centralized JSON serialization options: 10. Tool → AI Assistant (JSON via ToMcpJson()) ``` +> **Note:** For Pipeline and Dataflow tools, the flow includes a Handler layer: +> `Tool → Handler → Service → HttpClient → API`. The handler owns all business +> logic and returns `ToolResult`, which the tool maps to an MCP response. + ### Dataflow Query Execution Flow ``` @@ -936,6 +1031,40 @@ mcpBuilder.RegisterToolWithFeatureFlag( configuration, args, "new-feature-flag", nameof(NewTool), logger); ``` +#### Alternative: Handler-Based Tools (Recommended for shared logic) + +For tools whose logic needs to be shared across multiple MCP frameworks, use the handler pattern: + +1. Create a handler in `Core/Handlers/`: +```csharp +public class NewHandler(INewService service) +{ + public async Task> DoOperationAsync(string parameter) + { + try + { + ArgumentException.ThrowIfNullOrWhiteSpace(parameter); + var result = await service.DoOperationAsync(parameter); + return ToolResult.Success(new NewResult(result)); + } + catch (Exception ex) + { + return ToolResult.Failure(ex.Message); + } + } +} +``` + +2. Create a thin tool wrapper: +```csharp +[McpServerTool, Description("Description of the tool")] +public async Task NewOperationAsync(string parameter) +{ + var result = await _handler.DoOperationAsync(parameter); + return result.ToMcpToolResponse(); +} +``` + ### Adding New Services 1. Define interface in `Abstractions/Interfaces/`: