diff --git a/src/ProjectTemplates/GeneratedContent.targets b/src/ProjectTemplates/GeneratedContent.targets index c3287408034..ce6ba2bc502 100644 --- a/src/ProjectTemplates/GeneratedContent.targets +++ b/src/ProjectTemplates/GeneratedContent.targets @@ -10,6 +10,7 @@ <_LocalChatTemplateVariant>aspire <_ChatWithCustomDataContentRoot>$(MSBuildThisFileDirectory)Microsoft.Extensions.AI.Templates\src\ChatWithCustomData\ + <_McpServerContentRoot>$(MSBuildThisFileDirectory)Microsoft.Extensions.AI.Templates\src\McpServer\ @@ -34,9 +35,11 @@ 1.14.0 11.6.0 9.4.1-beta.291 + 10.0.0-preview.5.25277.114 9.3.0 1.53.0 1.53.0-preview + 0.3.0-preview.1 5.1.18 1.12.0 0.1.10 @@ -64,9 +67,11 @@ TemplatePackageVersion_AzureIdentity=$(TemplatePackageVersion_AzureIdentity); TemplatePackageVersion_AzureSearchDocuments=$(TemplatePackageVersion_AzureSearchDocuments); TemplatePackageVersion_CommunityToolkitAspire=$(TemplatePackageVersion_CommunityToolkitAspire); + TemplatePackageVersion_MicrosoftExtensionsHosting=$(TemplatePackageVersion_MicrosoftExtensionsHosting); TemplatePackageVersion_MicrosoftExtensionsServiceDiscovery=$(TemplatePackageVersion_MicrosoftExtensionsServiceDiscovery); TemplatePackageVersion_MicrosoftSemanticKernel=$(TemplatePackageVersion_MicrosoftSemanticKernel); TemplatePackageVersion_MicrosoftSemanticKernel_Preview=$(TemplatePackageVersion_MicrosoftSemanticKernel_Preview); + TemplatePackageVersion_ModelContextProtocol=$(TemplatePackageVersion_ModelContextProtocol); TemplatePackageVersion_OllamaSharp=$(TemplatePackageVersion_OllamaSharp); TemplatePackageVersion_OpenTelemetry=$(TemplatePackageVersion_OpenTelemetry); TemplatePackageVersion_PdfPig=$(TemplatePackageVersion_PdfPig); @@ -100,6 +105,9 @@ + <_GeneratedContentEnablingJustBuiltPackages diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj index 2827734c794..7784747028e 100644 --- a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/Microsoft.Extensions.AI.Templates.csproj @@ -61,6 +61,16 @@ **\NuGet.config; **\Directory.Build.targets; **\Directory.Build.props;" /> + + + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json new file mode 100644 index 00000000000..d4b9d0edf5b --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.mcp/server.json @@ -0,0 +1,20 @@ +{ + "description": "", + "name": "io.github./", + "packages": [ + { + "registry_name": "nuget", + "name": "", + "version": "0.1.0-beta", + "package_arguments": [], + "environment_variables": [] + } + ], + "repository": { + "url": "https://github.com//", + "source": "github" + }, + "version_detail": { + "version": "0.1.0-beta" + } +} diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/dotnetcli.host.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/dotnetcli.host.json new file mode 100644 index 00000000000..5be51dd6357 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/dotnetcli.host.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/dotnetcli.host", + "symbolInfo": {}, + "usageExamples": [ + "" + ] +} \ No newline at end of file diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide.host.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide.host.json new file mode 100644 index 00000000000..5edf447bbd4 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide.host.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/ide.host", + "order": 0, + "icon": "ide/icon.ico", + "symbolInfo": [] +} diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide/icon.ico b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide/icon.ico new file mode 100644 index 00000000000..954709ffd6b Binary files /dev/null and b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/ide/icon.ico differ diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/template.json b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/template.json new file mode 100644 index 00000000000..1fdc9128e81 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/.template.config/template.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Microsoft", + "classifications": [ + "Common", + "AI", + "MCP" + ], + "identity": "Microsoft.Extensions.AI.Templates.McpServer.CSharp", + "name": "Local MCP Server Console App", + "description": "A project template for creating a Model Context Protocol (MCP) server using C# and the ModelContextProtocol package.", + "shortName": "mcpserver", + "defaultName": "McpServer", + "sourceName": "McpServer-CSharp", + "preferNameDirectory": true, + "tags": { + "language": "C#", + "type": "project" + }, + "symbols": { + "hostIdentifier": { + "type": "bind", + "binding": "HostIdentifier" + } + }, + "primaryOutputs": [ + { + "path": "./README.md" + }, + { + "path": "./McpServer-CSharp.csproj" + } + ], + "postActions": [ + { + "condition": "(hostIdentifier != \"dotnetcli\" && hostIdentifier != \"dotnetcli-preview\")", + "description": "Opens README file in the editor", + "manualInstructions": [], + "actionId": "84C0DA21-51C8-4541-9940-6CA19AF04EE6", + "args": { + "files": "0" + }, + "continueOnError": true + } + ] +} diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/McpServer-CSharp.csproj.in b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/McpServer-CSharp.csproj.in new file mode 100644 index 00000000000..d47952229d9 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/McpServer-CSharp.csproj.in @@ -0,0 +1,32 @@ + + + + net10.0 + Exe + enable + enable + + + true + McpServer + + + README.md + SampleMcpServer + 0.1.0-beta + AI; MCP; server; stdio + An MCP server using the MCP C# SDK. + + + + + + + + + + + + + + diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Program.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Program.cs new file mode 100644 index 00000000000..f320c93fd88 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Program.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var builder = Host.CreateApplicationBuilder(args); + +// Configure all logs to go to stderr (stdout is used for the MCP protocol messages). +builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace); + +// Add the MCP services: the transport to use (stdio) and the tools to register. +builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithTools(); + +await builder.Build().RunAsync(); diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md new file mode 100644 index 00000000000..dc6f5038b61 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/README.md @@ -0,0 +1,82 @@ +# MCP Server + +This README was created using the C# MCP server template project. It demonstrates how you can easily create an MCP server using C# and then package it in a NuGet package. + +See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide. + +## Checklist before publishing to NuGet.org + +- Test the MCP server locally using the steps below. +- Update the package metadata in the .csproj file, in particular the ``. +- Update `.mcp/server.json` to declare your MCP server's inputs. + - See [configuring inputs](https://aka.ms/nuget/mcp/guide/configuring-inputs) for more details. +- Pack the project using `dotnet pack`. + +The `bin/Release` directory will contain the package file (.nupkg), which can be [published to NuGet.org](https://learn.microsoft.com/nuget/nuget-org/publish-a-package). + +## Using the MCP Server in VS Code + +Once the MCP server package is published to NuGet.org, you can use the following VS Code user configuration to download and install the MCP server package. See [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more information about using MCP servers in VS Code. + +```json +{ + "mcp": { + "servers": { + "McpServer-CSharp": { + "type": "stdio", + "command": "dotnet", + "args": [ + "tool", + "exec", + "", + "--version", + "", + "--yes" + ] + } + } + } +} +``` + +Now you can ask Copilot Chat for a random number, for example, `Give me 3 random numbers`. It should prompt you to use the `get_random_number` tool on the `McpServer-CSharp` MCP server and show you the results. + +## Developing locally in VS Code + +To test this MCP server from source code (locally) without using a built MCP server package, create a `.vscode/mcp.json` file (a VS Code workspace settings file) in your project directory and add the following configuration: + +```json +{ + "servers": { + "McpServer-CSharp": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] + } + } +} +``` + +Alternatively, you can configure your VS Code user settings to use your local project: + +```json +{ + "mcp": { + "servers": { + "McpServer-CSharp": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] + } + } + } +} +``` diff --git a/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs new file mode 100644 index 00000000000..4542f8505a5 --- /dev/null +++ b/src/ProjectTemplates/Microsoft.Extensions.AI.Templates/src/McpServer/McpServer-CSharp/Tools/RandomNumberTools.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using ModelContextProtocol.Server; + +/// +/// Sample MCP tools for demonstration purposes. +/// These tools can be invoked by MCP clients to perform various operations. +/// +internal class RandomNumberTools +{ + [McpServerTool(Name = "get_random_number")] + [Description("Generates a random number between the specified minimum and maximum values.")] + public int GetRandomNumber( + [Description("Minimum value (inclusive)")] int min = 0, + [Description("Maximum value (exclusive)")] int max = 100) + { + return Random.Shared.Next(min, max); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/McpServerSnapshotTests.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/McpServerSnapshotTests.cs new file mode 100644 index 00000000000..a3f3dedd1b5 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/McpServerSnapshotTests.cs @@ -0,0 +1,95 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.AI.Templates.Tests; +using Microsoft.Extensions.Logging; +using Microsoft.TemplateEngine.Authoring.TemplateVerifier; +using Microsoft.TemplateEngine.TestHelper; +using Xunit; +using Xunit.Abstractions; + +namespace Microsoft.Extensions.AI.Templates.Tests; + +public class McpServerSnapshotTests +{ + // Keep the exclude patterns below in sync with those in Microsoft.Extensions.AI.Templates.csproj. + private static readonly string[] _verificationExcludePatterns = [ + "**/bin/**", + "**/obj/**", + "**/.vs/**", + "**/*.sln", + "**/*.in", + ]; + + private readonly ILogger _log; + + public McpServerSnapshotTests(ITestOutputHelper log) + { +#pragma warning disable CA2000 // Dispose objects before losing scope + _log = new XunitLoggerProvider(log).CreateLogger("TestRun"); +#pragma warning restore CA2000 // Dispose objects before losing scope + } + + [Fact] + public async Task BasicTest() + { + await TestTemplateCoreAsync(scenarioName: "Basic"); + } + + private async Task TestTemplateCoreAsync(string scenarioName, IEnumerable? templateArgs = null) + { + string workingDir = TestUtils.CreateTemporaryFolder(); + string templateShortName = "mcpserver"; + + // Get the template location + string templateLocation = Path.Combine(WellKnownPaths.TemplateFeedLocation, "Microsoft.Extensions.AI.Templates", "src", "McpServer"); + + var verificationExcludePatterns = Path.DirectorySeparatorChar is '/' + ? _verificationExcludePatterns + : _verificationExcludePatterns.Select(p => p.Replace('/', Path.DirectorySeparatorChar)).ToArray(); + + TemplateVerifierOptions options = new TemplateVerifierOptions(templateName: templateShortName) + { + TemplatePath = templateLocation, + TemplateSpecificArgs = templateArgs, + SnapshotsDirectory = "Snapshots", + OutputDirectory = workingDir, + DoNotPrependCallerMethodNameToScenarioName = true, + DoNotAppendTemplateArgsToScenarioName = true, + ScenarioName = scenarioName, + VerificationExcludePatterns = verificationExcludePatterns, + } + .WithCustomScrubbers( + ScrubbersDefinition.Empty.AddScrubber((path, content) => + { + string filePath = path.UnixifyDirSeparators(); + + if (filePath.EndsWith(".csproj")) + { + // Scrub references to just-built packages and remove the suffix, if it exists. + // This allows the snapshots to remain the same regardless of where the repo is built (e.g., locally, public CI, internal CI). + var pattern = @"(?<=)"; + content.ScrubByRegex(pattern, replacement: "$1"); + } + })); + + VerificationEngine engine = new VerificationEngine(_log); + await engine.Execute(options); + +#pragma warning disable CA1031 // Do not catch general exception types + try + { + Directory.Delete(workingDir, recursive: true); + } + catch + { + /* don't care */ + } +#pragma warning restore CA1031 // Do not catch general exception types + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json new file mode 100644 index 00000000000..ab997541e52 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/.mcp/server.json @@ -0,0 +1,20 @@ +{ + "description": "", + "name": "io.github./", + "packages": [ + { + "registry_name": "nuget", + "name": "", + "version": "0.1.0-beta", + "package_arguments": [], + "environment_variables": [] + } + ], + "repository": { + "url": "https://github.com//", + "source": "github" + }, + "version_detail": { + "version": "0.1.0-beta" + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Program.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Program.cs new file mode 100644 index 00000000000..73b72d35a46 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Program.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var builder = Host.CreateApplicationBuilder(args); + +// Configure all logs to go to stderr (stdout is used for the MCP protocol messages). +builder.Logging.AddConsole(o => o.LogToStandardErrorThreshold = LogLevel.Trace); + +// Add the MCP services: the transport to use (stdio) and the tools to register. +builder.Services + .AddMcpServer() + .WithStdioServerTransport() + .WithTools(); + +await builder.Build().RunAsync(); diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md new file mode 100644 index 00000000000..25704e5d135 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/README.md @@ -0,0 +1,82 @@ +# MCP Server + +This README was created using the C# MCP server template project. It demonstrates how you can easily create an MCP server using C# and then package it in a NuGet package. + +See [aka.ms/nuget/mcp/guide](https://aka.ms/nuget/mcp/guide) for the full guide. + +## Checklist before publishing to NuGet.org + +- Test the MCP server locally using the steps below. +- Update the package metadata in the .csproj file, in particular the ``. +- Update `.mcp/server.json` to declare your MCP server's inputs. + - See [configuring inputs](https://aka.ms/nuget/mcp/guide/configuring-inputs) for more details. +- Pack the project using `dotnet pack`. + +The `bin/Release` directory will contain the package file (.nupkg), which can be [published to NuGet.org](https://learn.microsoft.com/nuget/nuget-org/publish-a-package). + +## Using the MCP Server in VS Code + +Once the MCP server package is published to NuGet.org, you can use the following VS Code user configuration to download and install the MCP server package. See [Use MCP servers in VS Code (Preview)](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) for more information about using MCP servers in VS Code. + +```json +{ + "mcp": { + "servers": { + "mcpserver": { + "type": "stdio", + "command": "dotnet", + "args": [ + "tool", + "exec", + "", + "--version", + "", + "--yes" + ] + } + } + } +} +``` + +Now you can ask Copilot Chat for a random number, for example, `Give me 3 random numbers`. It should prompt you to use the `get_random_number` tool on the `mcpserver` MCP server and show you the results. + +## Developing locally in VS Code + +To test this MCP server from source code (locally) without using a built MCP server package, create a `.vscode/mcp.json` file (a VS Code workspace settings file) in your project directory and add the following configuration: + +```json +{ + "servers": { + "mcpserver": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] + } + } +} +``` + +Alternatively, you can configure your VS Code user settings to use your local project: + +```json +{ + "mcp": { + "servers": { + "mcpserver": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] + } + } + } +} +``` diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs new file mode 100644 index 00000000000..72af767e320 --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/Tools/RandomNumberTools.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using ModelContextProtocol.Server; + +/// +/// Sample MCP tools for demonstration purposes. +/// These tools can be invoked by MCP clients to perform various operations. +/// +internal class RandomNumberTools +{ + [McpServerTool(Name = "get_random_number")] + [Description("Generates a random number between the specified minimum and maximum values.")] + public int GetRandomNumber( + [Description("Minimum value (inclusive)")] int min = 0, + [Description("Maximum value (exclusive)")] int max = 100) + { + return Random.Shared.Next(min, max); + } +} diff --git a/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj new file mode 100644 index 00000000000..a71ac148e6f --- /dev/null +++ b/test/ProjectTemplates/Microsoft.Extensions.AI.Templates.IntegrationTests/Snapshots/mcpserver.Basic.verified/mcpserver/mcpserver.csproj @@ -0,0 +1,32 @@ + + + + net10.0 + Exe + enable + enable + + + true + McpServer + + + README.md + SampleMcpServer + 0.1.0-beta + AI; MCP; server; stdio + An MCP server using the MCP C# SDK. + + + + + + + + + + + + + +