Add ci build workflow (#31)

* Add ci build workflow

* Using cake build

* Update readme
This commit is contained in:
Toni Wenzel 2025-03-06 20:49:59 +01:00 • committed by GitHub
commit 80daa5b73d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 303 additions and 20 deletions

13
.config/dotnet-tools.json Normal file
View file

@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"cake.tool": {
"version": "5.0.0",
"commands": [
"dotnet-cake"
],
"rollForward": false
}
}
}

43
.github/workflows/ci.yml vendored Normal file
View file

@ -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 }}

5
.gitignore vendored
View file

@ -1,3 +1,8 @@
# Cake tools
[Tt]ools/
# Build output
[Bb]uildArtifacts/
# Build results
[Dd]ebug/
[Rr]elease/

2
GitVersion.yml Normal file
View file

@ -0,0 +1,2 @@
mode: ContinuousDeployment
commit-message-incrementing: Disabled

View file

@ -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.

179
build.cake Normal file
View file

@ -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);

13
build.ps1 Normal file
View file

@ -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 }

12
build.sh Executable file
View file

@ -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 "$@"

View file

@ -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

View file

@ -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.

View file

@ -5,6 +5,7 @@ using McpDotNet.Protocol.Types;
namespace McpDotNet.Tests;
[Trait("Execution", "Manual")]
public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture>
{
private readonly ClientIntegrationTestFixture _fixture;
@ -18,7 +19,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
public async Task ConnectAndPing_Stdio_EverythingServer()
{
// Arrange
// Act
var client = await _fixture.Factory.GetClientAsync("everything");
await client.PingAsync(CancellationToken.None);
@ -47,7 +48,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
public async Task ListTools_Stdio_EverythingServer()
{
// arrange
// act
var client = await _fixture.Factory.GetClientAsync("everything");
var tools = await client.ListToolsAsync();
@ -62,7 +63,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
public async Task CallTool_Stdio_EchoServer()
{
// arrange
// act
var client = await _fixture.Factory.GetClientAsync("everything");
var result = await client.CallToolAsync(
@ -184,7 +185,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
public async Task ReadResource_Stdio_BinaryResource()
{
// arrange
// act
var client = await _fixture.Factory.GetClientAsync("everything");
// Even numbered resources are binary in the everything server (despite the docs saying otherwise)
@ -214,11 +215,11 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// act
var client = await _fixture.Factory.GetClientAsync("everything");
var result = await client.GetCompletionAsync(new Reference
{
Type = "ref/resource",
Uri = "test://static/resource/1"
},
"argument_name", "1",
{
Type = "ref/resource",
Uri = "test://static/resource/1"
},
"argument_name", "1",
CancellationToken.None
);
@ -249,7 +250,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
Type = "ref/prompt",
Name = "irrelevant"
},
argumentName:"style", argumentValue:"fo",
argumentName: "style", argumentValue: "fo",
CancellationToken.None
);
@ -293,7 +294,7 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// Set up the sampling handler
int samplingHandlerCalls = 0;
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
client.SamplingHandler = async (_,_) =>
client.SamplingHandler = async (_, _) =>
{
samplingHandlerCalls++;
return new CreateMessageResult
@ -312,10 +313,10 @@ public class ClientIntegrationTests : IClassFixture<ClientIntegrationTestFixture
// Call the server's sampleLLM tool which should trigger our sampling handler
var result = await client.CallToolAsync(
"sampleLLM",
new Dictionary<string, object>
new Dictionary<string, object>
{
["prompt"] = "Test prompt",
["maxTokens"] = 100
["prompt"] = "Test prompt",
["maxTokens"] = 100
}
);

View file

@ -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<TestSseServer>());
await using TestSseServer server = new(logger: loggerFactory.CreateLogger<TestSseServer>());
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<TestSseServer>());
await server.StartAsync();
var defaultOptions = new McpClientOptions
{
ClientInfo = new() { Name = "IntegrationTestClient", Version = "1.0.0" }