From 80daa5b73d2e82b6cb8c674df0c5ca7d3295994e Mon Sep 17 00:00:00 2001 From: Toni Wenzel Date: Thu, 6 Mar 2025 20:49:59 +0100 Subject: [PATCH] Add ci build workflow (#31) * Add ci build workflow * Using cake build * Update readme --- .config/dotnet-tools.json | 13 ++ .github/workflows/ci.yml | 43 +++++ .gitignore | 5 + GitVersion.yml | 2 + README.MD | 9 + build.cake | 179 ++++++++++++++++++ build.ps1 | 13 ++ build.sh | 12 ++ mcpdotnet.sln | 5 + .../IntegrationTests.cs | 1 + .../mcpdotnet.Tests/ClientIntegrationTests.cs | 29 +-- tests/mcpdotnet.Tests/SseIntegrationTests.cs | 12 +- 12 files changed, 303 insertions(+), 20 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100644 .github/workflows/ci.yml create mode 100644 GitVersion.yml create mode 100644 build.cake create mode 100644 build.ps1 create mode 100755 build.sh diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000..b6554f3 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "cake.tool": { + "version": "5.0.0", + "commands": [ + "dotnet-cake" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2e0a2f5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: .NET Build, Test and publish + +on: + push: + branches: [ "main", "master" ] + paths: [ 'src/**' ] + pull_request: + branches: [ "main", "master" ] + paths: [ 'src/**' ] + release: + types: [published,edited] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Fetch all history for all tags and branches + run: git fetch --prune --unshallow + + - name: Setup .NET + uses: actions/setup-dotnet@v2 + with: + dotnet-version: | + 8.0.x + 9.0.x + + - name: Run Cake script + uses: cake-build/cake-action@v1 + with: + verbosity: Diagnostic + env: + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY}} + SONAR_LOGIN: ${{ secrets.SONAR_LOGIN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 09856b4..1c7489e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +# Cake tools +[Tt]ools/ + +# Build output +[Bb]uildArtifacts/ # Build results [Dd]ebug/ [Rr]elease/ diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 0000000..b219700 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,2 @@ +mode: ContinuousDeployment +commit-message-incrementing: Disabled \ No newline at end of file diff --git a/README.MD b/README.MD index d5c3d17..38f8460 100644 --- a/README.MD +++ b/README.MD @@ -1,5 +1,14 @@ # mcpdotnet [![NuGet version](https://img.shields.io/nuget/v/mcpdotnet.svg)](https://www.nuget.org/packages/mcpdotnet/) +![Build](https://github.com/PederHP/mcpdotnet/actions/workflows/ci.yml/badge.svg?branch=main) +[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=PederHP_mcpdotnet&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=PederHP_mcpdotnet) +[![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=PederHP_mcpdotnet&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=PederHP_mcpdotnet) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=PederHP_mcpdotnet&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=PederHP_mcpdotnet) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=PederHP_mcpdotnet&metric=bugs)](https://sonarcloud.io/summary/new_code?id=PederHP_mcpdotnet) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=PederHP_mcpdotnet&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=PederHP_mcpdotnet) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=PederHP_mcpdotnet&metric=coverage)](https://sonarcloud.io/summary/new_code?id=PederHP_mcpdotnet) A .NET implementation of the Model Context Protocol (MCP), enabling .NET applications to connect to and interact with MCP clients and servers. diff --git a/build.cake b/build.cake new file mode 100644 index 0000000..80b814b --- /dev/null +++ b/build.cake @@ -0,0 +1,179 @@ +#tool "dotnet:?package=GitVersion.Tool&version=6.1.0" +#tool "nuget:?package=dotnet-sonarscanner&version=9.2.1" + +#addin "nuget:?package=Cake.Sonar&version=1.1.33" + +var target = Argument("target", "Default"); +var sonarLoginToken = Argument("sonarLogin", EnvironmentVariable("SONAR_LOGIN") ?? ""); +var nugetApiKey = Argument("nugetApiKey", EnvironmentVariable("NUGET_API_KEY") ?? ""); + +////////////////////////////////////////////////////////////////////// +// Build Variables +///////////////////////////////////////////////////////////////////// +var solution = "./mcpdotnet.sln"; +var project = "./src/mcpdotnet.csproj"; +var outputDir = MakeAbsolute(Directory("./buildArtifacts/")); +var outputDirNuget = outputDir.Combine("NuGet"); +var sonarProjectKey = "PederHP_mcpdotnet"; +var sonarUrl = "https://sonarcloud.io"; +var sonarOrganization = "pederhp"; + +var outputDirTemp = outputDir.Combine("Temp"); +var outputDirTests = outputDirTemp.Combine("Tests"); + +var codeCoverageResultFilePath = MakeAbsolute(outputDirTests).Combine("**/").CombineWithFilePath("coverage.opencover.xml"); +var testResultsPath = MakeAbsolute(outputDirTests).CombineWithFilePath("*.trx"); + +var nugetPublishFeed = "https://api.nuget.org/v3/index.json"; + +var isLocalBuild = BuildSystem.IsLocalBuild; +var isMainBranch = StringComparer.OrdinalIgnoreCase.Equals("refs/heads/main", BuildSystem.GitHubActions.Environment.Workflow.Ref); +var isPullRequest = BuildSystem.GitHubActions.Environment.PullRequest.IsPullRequest; +var runSonar = !string.IsNullOrWhiteSpace(sonarLoginToken); + +var gitHubEvent = EnvironmentVariable("GITHUB_EVENT_NAME"); +var isReleaseCreation = string.Equals(gitHubEvent, "release"); +GitVersion versionInfo = null; + +////////////////////////////////////////////////////////////////////// +// TASKS +////////////////////////////////////////////////////////////////////// + +Setup(context => +{ + Information($"Local build: {isLocalBuild}"); + Information($"Main branch: {isMainBranch}"); + Information($"Pull request: {isPullRequest}"); + Information($"Run sonar: {runSonar}"); + Information($"ref: {BuildSystem.GitHubActions.Environment.Workflow.Ref}"); + Information($"Is release creation: {isReleaseCreation}"); +}); + +Task("Clean") + .Description("Removes the output directory") + .Does(() => + { + + EnsureDirectoryDoesNotExist(outputDir, new DeleteDirectorySettings + { + Recursive = true, + Force = true + }); + CreateDirectory(outputDir); + }); + + +Task("Version") + .Description("Retrieves the current version from the git repository") + .Does(() => + { + versionInfo = GitVersion(new GitVersionSettings + { + UpdateAssemblyInfo = false + }); + + Information("Version: " + versionInfo.FullSemVer); + }); + +Task("Build") + .IsDependentOn("Clean") + .IsDependentOn("Version") + .Does(() => + { + var msBuildSettings = new DotNetMSBuildSettings() + { + Version = versionInfo.AssemblySemVer, + InformationalVersion = versionInfo.InformationalVersion, + PackageVersion = versionInfo.SemVer + }.WithProperty("PackageOutputPath", outputDirNuget.FullPath); + + var settings = new DotNetBuildSettings + { + Configuration = "Release", + MSBuildSettings = msBuildSettings + }; + + DotNetBuild(solution, settings); + }); + +Task("Test") + .IsDependentOn("Build") + .Does(() => + { + var settings = new DotNetTestSettings + { + Configuration = "Release", + Loggers = new[] { "trx;" }, + ResultsDirectory = outputDirTests, + Collectors = new[] { "XPlat Code Coverage" }, + Filter = "(Execution!=Manual)", + ArgumentCustomization = a => a.Append("-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover"), + NoBuild = true + }; + + DotNetTest(solution, settings); + }); + +Task("SonarBegin") + .WithCriteria(runSonar) + .Does(() => + { + SonarBegin(new SonarBeginSettings + { + Key = sonarProjectKey, + Url = sonarUrl, + Organization = sonarOrganization, + Token = sonarLoginToken, + UseCoreClr = true, + VsTestReportsPath = testResultsPath.ToString(), + OpenCoverReportsPath = codeCoverageResultFilePath.ToString() + }); + }); + +Task("SonarEnd") + .WithCriteria(runSonar) + .Does(() => + { + SonarEnd(new SonarEndSettings + { + Token = sonarLoginToken + }); + }); + +Task("Publish") + .WithCriteria(isReleaseCreation) + .IsDependentOn("Test") + .IsDependentOn("Version") + .Description("Pushes the created NuGet packages to nuget.org") + .Does(() => + { + Information($"Upload packages from {outputDirNuget.FullPath}"); + + // Get the paths to the packages ordered by the file names in order to get the nupkg first. + var packages = GetFiles(outputDirNuget.CombineWithFilePath("*.*nupkg").ToString()).OrderBy(x => x.FullPath).ToArray(); + + if (packages.Length == 0) + { + Error("No packages found to upload"); + return; + } + + // Push the package and symbols + foreach (var package in packages) + { + DotNetNuGetPush(package, new DotNetNuGetPushSettings + { + Source = nugetPublishFeed, + ApiKey = nugetApiKey, + SkipDuplicate = true + }); + } + }); + +Task("Default") + .IsDependentOn("SonarBegin") + .IsDependentOn("Test") + .IsDependentOn("SonarEnd") + .IsDependentOn("Publish"); + +RunTarget(target); \ No newline at end of file diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..21821d2 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,13 @@ +$ErrorActionPreference = 'Stop' + +Set-Location -LiteralPath $PSScriptRoot + +$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1' +$env:DOTNET_CLI_TELEMETRY_OPTOUT = '1' +$env:DOTNET_NOLOGO = '1' + +dotnet tool restore +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +dotnet cake @args +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..31be886 --- /dev/null +++ b/build.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euox pipefail + +cd "$(dirname "${BASH_SOURCE[0]}")" + +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_NOLOGO=1 + +dotnet tool restore + +dotnet cake "$@" diff --git a/mcpdotnet.sln b/mcpdotnet.sln index 5781724..38eb5a5 100644 --- a/mcpdotnet.sln +++ b/mcpdotnet.sln @@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestServerWithHosting", "sa EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{2A77AF5C-138A-4EBB-9A13-9205DCD67928}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -64,8 +66,11 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution + {FF41F619-833D-4FA2-8C53-04D0A1D5AA61} = {2A77AF5C-138A-4EBB-9A13-9205DCD67928} {76E295FA-9E85-7B99-332A-2CDBFCD5860A} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {CA0BB450-1903-2F92-A68D-1285976551D6} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {7C229573-A085-4ECC-8131-958CDA2BE731} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {67BEF596-AD39-4BDD-B505-31DE7FF0C8D2} = {2A77AF5C-138A-4EBB-9A13-9205DCD67928} {6499876E-2F76-44A8-B6EB-5B889C6E9B7F} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/tests/McpDotNet.Extensions.AI.Tests/IntegrationTests.cs b/tests/McpDotNet.Extensions.AI.Tests/IntegrationTests.cs index 71bf8a5..ec57ba0 100644 --- a/tests/McpDotNet.Extensions.AI.Tests/IntegrationTests.cs +++ b/tests/McpDotNet.Extensions.AI.Tests/IntegrationTests.cs @@ -6,6 +6,7 @@ using OpenAI; namespace McpDotNet.Extensions.AI.Tests; +[Trait("Execution", "Manual")] public class IntegrationTests { private const string OpenAIKey = ""; // Provide your own key when running the test. Do not commit it. diff --git a/tests/mcpdotnet.Tests/ClientIntegrationTests.cs b/tests/mcpdotnet.Tests/ClientIntegrationTests.cs index d7a9bc6..ff922c3 100644 --- a/tests/mcpdotnet.Tests/ClientIntegrationTests.cs +++ b/tests/mcpdotnet.Tests/ClientIntegrationTests.cs @@ -5,6 +5,7 @@ using McpDotNet.Protocol.Types; namespace McpDotNet.Tests; +[Trait("Execution", "Manual")] public class ClientIntegrationTests : IClassFixture { private readonly ClientIntegrationTestFixture _fixture; @@ -18,7 +19,7 @@ public class ClientIntegrationTests : IClassFixture + client.SamplingHandler = async (_, _) => { samplingHandlerCalls++; return new CreateMessageResult @@ -312,10 +313,10 @@ public class ClientIntegrationTests : IClassFixture + new Dictionary { - ["prompt"] = "Test prompt", - ["maxTokens"] = 100 + ["prompt"] = "Test prompt", + ["maxTokens"] = 100 } ); diff --git a/tests/mcpdotnet.Tests/SseIntegrationTests.cs b/tests/mcpdotnet.Tests/SseIntegrationTests.cs index 2b4e135..4afa75a 100644 --- a/tests/mcpdotnet.Tests/SseIntegrationTests.cs +++ b/tests/mcpdotnet.Tests/SseIntegrationTests.cs @@ -1,14 +1,14 @@ -using McpDotNet.Client; +using System.Text.Json; +using McpDotNet.Client; using McpDotNet.Configuration; -using McpDotNet.Protocol.Messages; using McpDotNet.Protocol.Transport; using McpDotNet.Protocol.Types; using McpDotNet.Tests.Utils; using Microsoft.Extensions.Logging; -using System.Text.Json; namespace McpDotNet.Tests; +[Trait("Execution", "Manual")] public class SseIntegrationTests { [Fact] @@ -19,7 +19,7 @@ public class SseIntegrationTests builder.AddConsole() .SetMinimumLevel(LogLevel.Debug)); - await using TestSseServer server = new(logger:loggerFactory.CreateLogger()); + await using TestSseServer server = new(logger: loggerFactory.CreateLogger()); await server.StartAsync(); @@ -111,7 +111,7 @@ public class SseIntegrationTests ClientInfo = new() { Name = "IntegrationTestClient", - Version = "1.0.0" + Version = "1.0.0" }, Capabilities = new() { @@ -228,7 +228,7 @@ public class SseIntegrationTests await using TestSseServer server = new(logger: loggerFactory.CreateLogger()); await server.StartAsync(); - + var defaultOptions = new McpClientOptions { ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" }