diff --git a/AspNetCore.slnx b/AspNetCore.slnx index dac5f03bb6cb..917cdafc5f67 100644 --- a/AspNetCore.slnx +++ b/AspNetCore.slnx @@ -775,6 +775,7 @@ + diff --git a/eng/Build.props b/eng/Build.props index 2d0cf067a556..da1a3f26306f 100644 --- a/eng/Build.props +++ b/eng/Build.props @@ -45,6 +45,7 @@ $(RepoRoot)src\Components\Web.JS\node_modules\**\*.*proj; $(RepoRoot)src\Installers\**\*.*proj; $(RepoRoot)src\ProjectTemplates\Web.ProjectTemplates\content\**\*.*proj; + $(RepoRoot)src\ProjectTemplates\McpServer.ProjectTemplates\content\**\*.*proj; $(RepoRoot)src\SignalR\clients\ts\**\node_modules\**\*.*proj; " /> diff --git a/eng/Dependencies.props b/eng/Dependencies.props index ee7006b07210..39b1cef01649 100644 --- a/eng/Dependencies.props +++ b/eng/Dependencies.props @@ -215,6 +215,8 @@ may be turned into `` items in projects. + + diff --git a/eng/Versions.props b/eng/Versions.props index 0aac744e93b8..aeea0ec00b19 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -157,6 +157,8 @@ 3.14.1 0.3.269 $(MessagePackVersion) + 1.2.0 + $(ModelContextProtocolVersion) 4.10.0 0.11.2 2.2.1 diff --git a/src/ProjectTemplates/McpServer.ProjectTemplates/.gitignore b/src/ProjectTemplates/McpServer.ProjectTemplates/.gitignore new file mode 100644 index 000000000000..9310b1ea5295 --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/.gitignore @@ -0,0 +1,3 @@ +# This file is generated by the build +content/*/*.*proj +content/*/*/*.*proj diff --git a/src/ProjectTemplates/McpServer.ProjectTemplates/McpServer-Local-CSharp.csproj.in b/src/ProjectTemplates/McpServer.ProjectTemplates/McpServer-Local-CSharp.csproj.in new file mode 100644 index 000000000000..e082eb4fe7a2 --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/McpServer-Local-CSharp.csproj.in @@ -0,0 +1,52 @@ + + + + ${DefaultNetCoreTargetFramework} + + win-x64;win-arm64;osx-arm64;linux-x64;linux-arm64;linux-musl-x64 + + Major + + Exe + enable + enable + + + true + McpServer + + + + true + true + + + true + + + + + true + true + + + + README.md + SampleMcpServer + 0.1.0-beta + AI; MCP; server; stdio + An MCP server using the MCP C# SDK. + + + + + + + + + + + + + + diff --git a/src/ProjectTemplates/McpServer.ProjectTemplates/McpServer-Remote-CSharp.csproj.in b/src/ProjectTemplates/McpServer.ProjectTemplates/McpServer-Remote-CSharp.csproj.in new file mode 100644 index 000000000000..bfc1b385b16f --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/McpServer-Remote-CSharp.csproj.in @@ -0,0 +1,35 @@ + + + + ${DefaultNetCoreTargetFramework} + + win-x64;win-arm64;osx-arm64;linux-x64;linux-arm64;linux-musl-x64 + + Major + + enable + enable + aaaaaaaa-bbbb-cccc-dddd-eeeeeeffffff + + + + + true + true + + + true + + + + + true + true + + + + + + + + diff --git a/src/ProjectTemplates/McpServer.ProjectTemplates/Microsoft.McpServer.ProjectTemplates.csproj b/src/ProjectTemplates/McpServer.ProjectTemplates/Microsoft.McpServer.ProjectTemplates.csproj new file mode 100644 index 000000000000..ed9df779964e --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/Microsoft.McpServer.ProjectTemplates.csproj @@ -0,0 +1,24 @@ + + + + $(DefaultNetCoreTargetFramework) + Microsoft.McpServer.ProjectTemplates.$(AspNetCoreMajorMinorVersion) + MCP Server Template Pack for Microsoft Template Engine + + + + + + DefaultNetCoreTargetFramework=$(DefaultNetCoreTargetFramework); + MicrosoftExtensionsHostingVersion=$(MicrosoftExtensionsHostingVersion); + ModelContextProtocolVersion=$(ModelContextProtocolVersion); + ModelContextProtocolAspNetCoreVersion=$(ModelContextProtocolAspNetCoreVersion); + + + + + + + + + diff --git a/src/ProjectTemplates/McpServer.ProjectTemplates/content/Directory.Build.props b/src/ProjectTemplates/McpServer.ProjectTemplates/content/Directory.Build.props new file mode 100644 index 000000000000..5e2e6944540a --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/content/Directory.Build.props @@ -0,0 +1,8 @@ + + + + diff --git a/src/ProjectTemplates/McpServer.ProjectTemplates/content/Directory.Build.targets b/src/ProjectTemplates/McpServer.ProjectTemplates/content/Directory.Build.targets new file mode 100644 index 000000000000..0f803ab0e03f --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/content/Directory.Build.targets @@ -0,0 +1,7 @@ + + + + diff --git a/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/.template.config/dotnetcli.host.json b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/.template.config/dotnetcli.host.json new file mode 100644 index 000000000000..c371379f9042 --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/.template.config/dotnetcli.host.json @@ -0,0 +1,33 @@ +{ + "$schema": "https://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "Transport": { + "longName": "transport", + "shortName": "t" + }, + "NativeAot": { + "longName": "aot", + "shortName": "" + }, + "SelfContained": { + "longName": "self-contained", + "shortName": "" + }, + "Framework": { + "longName": "framework" + }, + "skipRestore": { + "longName": "no-restore", + "shortName": "" + }, + "httpPort": { + "isHidden": true + }, + "httpsPort": { + "isHidden": true + } + }, + "usageExamples": [ + "" + ] +} diff --git a/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/.template.config/ide.host.json b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/.template.config/ide.host.json new file mode 100644 index 000000000000..a4971fdf7fc4 --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/.template.config/ide.host.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/ide.host", + "order": 0, + "icon": "ide/icon.ico", + "symbolInfo": [ + { + "id": "Transport", + "isVisible": true + }, + { + "id": "NativeAot", + "isVisible": true + }, + { + "id": "SelfContained", + "isVisible": true + } + ] +} diff --git a/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/.template.config/ide/icon.ico b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/.template.config/ide/icon.ico new file mode 100644 index 000000000000..954709ffd6b9 Binary files /dev/null and b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/.template.config/ide/icon.ico differ diff --git a/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/.template.config/template.json b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/.template.config/template.json new file mode 100644 index 000000000000..a84a21c30518 --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/.template.config/template.json @@ -0,0 +1,223 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Microsoft", + "classifications": [ + "Common", + "AI", + "MCP" + ], + "name": "MCP Server App", + "description": "A project template for creating a Model Context Protocol (MCP) server using C# and the ModelContextProtocol package.", + "groupIdentity": "Microsoft.McpServer", + "precedence": "11000", + "identity": "Microsoft.McpServer.CSharp.11.0", + "shortName": "mcpserver", + "defaultName": "McpServer", + "sourceName": "McpServer-CSharp", + "preferNameDirectory": true, + "tags": { + "language": "C#", + "type": "project" + }, + "guids": [ + "aaaaaaaa-bbbb-cccc-dddd-eeeeeeffffff" + ], + "symbols": { + "hostIdentifier": { + "type": "bind", + "binding": "HostIdentifier" + }, + "Transport": { + "type": "parameter", + "displayName": "The MCP server _transport type to use", + "description": "Whether to create a 'local' (stdio transport) or 'remote' (http transport) MCP server", + "datatype": "choice", + "choices": [ + { + "choice": "local", + "description": "A console application will be created to use the stdio transport as a local MCP server" + }, + { + "choice": "remote", + "description": "An ASP.NET Core web application will be created to use the http transport as a remote MCP server" + } + ], + "defaultValue": "local" + }, + "NativeAot": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "false", + "displayName": "Enable _native AOT publish", + "description": "Whether to enable the MCP server for publishing as a native AOT application." + }, + "SelfContained": { + "type": "parameter", + "datatype": "bool", + "defaultValue": "true", + "displayName": "Enable _self-contained publish", + "description": "Whether to enable the MCP server for publishing as a self-contained application." + }, + "Framework": { + "type": "parameter", + "description": "The target framework for the project.", + "datatype": "choice", + "choices": [ + { + "choice": "net11.0" + } + ], + "replaces": "net11.0", + "defaultValue": "net11.0" + }, + "skipRestore": { + "type": "parameter", + "datatype": "bool", + "description": "If specified, skips the automatic restore of the project on create.", + "defaultValue": "false" + }, + "httpsPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use for the HTTPS endpoint in launchSettings.json." + }, + "httpsPortGenerated": { + "type": "generated", + "generator": "port", + "parameters": { + "low": 5000, + "high": 5300 + } + }, + "httpsPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "httpsPort", + "fallbackVariableName": "httpsPortGenerated" + }, + "replaces": "9995", + "onlyIf": [{ + "after": "localhost:" + }] + }, + "httpPort": { + "type": "parameter", + "datatype": "integer", + "description": "Port number to use for the HTTP endpoint in launchSettings.json." + }, + "httpPortGenerated": { + "type": "generated", + "generator": "port", + "parameters": { + "low": 6000, + "high": 6300 + } + }, + "httpPortReplacer": { + "type": "generated", + "generator": "coalesce", + "parameters": { + "sourceVariableName": "httpPort", + "fallbackVariableName": "httpPortGenerated" + }, + "replaces": "9996", + "onlyIf": [{ + "after": "localhost:" + }] + }, + "IsTransportRemote": { + "type": "computed", + "value": "(Transport == \"remote\")" + }, + "IsTransportLocal": { + "type": "computed", + "value": "(!IsTransportRemote)" + } + }, + "primaryOutputs": [ + { + "path": "./README.md" + }, + { + "path": "./McpServer-CSharp.csproj" + } + ], + "sources": [ + { + "source": "./common", + "target": "./" + }, + { + "condition": "(IsTransportLocal)", + "source": "./local", + "target": "./" + }, + { + "condition": "(IsTransportRemote)", + "source": "./remote", + "target": "./" + } + ], + "postActions": [ + { + "id": "restore", + "condition": "(!skipRestore)", + "description": "Restore NuGet packages required by this project.", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "args": { + "files": ["**/*.csproj"] + }, + "continueOnError": true + }, + { + "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 + } + ], + "SpecialCustomOperations": { + "**/*.md": { + "operations": [ + { + "type": "conditional", + "configuration": { + "if": [ "#### ---#if" ], + "else": [ "#### ---#else" ], + "elseif": [ "#### ---#elseif", "#### ---#elif" ], + "endif": [ "#### ---#endif" ], + "trim": "true", + "wholeLine": "true", + "evaluator": "C++" + } + } + ] + }, + "**/*.http": { + "operations": [ + { + "type": "conditional", + "configuration": { + "if": [ "#if" ], + "else": [ "#else" ], + "elseif": [ "#elseif", "#elif" ], + "endif": [ "#endif" ], + "trim": "true", + "wholeLine": "true", + "evaluator": "C++" + } + } + ] + } + } +} diff --git a/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/common/Tools/RandomNumberTools.cs b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/common/Tools/RandomNumberTools.cs new file mode 100644 index 000000000000..568574f47d96 --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/common/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] + [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/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/local/.mcp/server.json b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/local/.mcp/server.json new file mode 100644 index 000000000000..f5b270270d0a --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/local/.mcp/server.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json", + "description": "", + "name": "io.github./", + "version": "0.1.0-beta", + "packages": [ + { + "registryType": "nuget", + "identifier": "", + "version": "0.1.0-beta", + "transport": { + "type": "stdio" + }, + "packageArguments": [], + "environmentVariables": [] + } + ], + "repository": { + "url": "https://github.com//", + "source": "github" + } +} diff --git a/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/local/Program.cs b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/local/Program.cs new file mode 100644 index 000000000000..f320c93fd888 --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/local/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/McpServer.ProjectTemplates/content/McpServer-CSharp/local/README.md b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/local/README.md new file mode 100644 index 000000000000..05c9ac5e26d5 --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/local/README.md @@ -0,0 +1,104 @@ +# MCP Server + +This README was created using the C# MCP server project template. +It demonstrates how you can easily create an MCP server using C# and publish it as a NuGet package. + +#### ---#if (SelfContained) +The MCP server is built as a self-contained application and does not require the .NET runtime to be installed on the target machine. +However, since it is self-contained, it must be built for each target platform separately. +By default, the template is configured to build for: +* `win-x64` +* `win-arm64` +* `osx-arm64` +* `linux-x64` +* `linux-arm64` +* `linux-musl-x64` + +If your users require more platforms to be supported, update the list of runtime identifiers in the project's `` element. +#### ---#else +The MCP server is built as a framework-dependent application and requires the .NET runtime to be installed on the target machine. +The application is configured to roll-forward to the next highest major version of the runtime if one is available on the target machine. +If an applicable .NET runtime is not available, the MCP server will not start. +Consider building the MCP server as a self-contained application if you want to avoid this dependency. +#### ---#endif + +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). + +## Developing locally + +To test this MCP server from source code (locally) without using a built MCP server package, you can configure your IDE to run the project directly using `dotnet run`. + +```json +{ + "servers": { + "McpServer-CSharp": { + "type": "stdio", + "command": "dotnet", + "args": [ + "run", + "--project", + "" + ] + } + } +} +``` + +Refer to the VS Code or Visual Studio documentation for more information on configuring and using MCP servers: + +- [Use MCP servers in VS Code](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) +- [Use MCP servers in Visual Studio](https://learn.microsoft.com/visualstudio/ide/mcp-servers) + +## Testing the MCP Server + +Once configured, 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. + +## Publishing to NuGet.org + +1. Run `dotnet pack -c Release` to create the NuGet package +2. Publish to NuGet.org with `dotnet nuget push bin/Release/*.nupkg --api-key --source https://api.nuget.org/v3/index.json` + +## Using the MCP Server from NuGet.org + +Once the MCP server package is published to NuGet.org, you can configure it in your preferred IDE. Both VS Code and Visual Studio use the `dnx` command to download and install the MCP server package from NuGet.org. + +- **VS Code**: Create a `/.vscode/mcp.json` file +- **Visual Studio**: Create a `\.mcp.json` file + +For both VS Code and Visual Studio, the configuration file uses the following server definition: + +```json +{ + "servers": { + "McpServer-CSharp": { + "type": "stdio", + "command": "dnx", + "args": [ + "", + "--version", + "", + "--yes" + ] + } + } +} +``` + +## More information + +.NET MCP servers use the [ModelContextProtocol](https://www.nuget.org/packages/ModelContextProtocol) C# SDK. For more information about MCP: + +- [Official Documentation](https://modelcontextprotocol.io/) +- [Protocol Specification](https://spec.modelcontextprotocol.io/) +- [GitHub Organization](https://github.com/modelcontextprotocol) +- [MCP C# SDK](https://csharp.sdk.modelcontextprotocol.io/) diff --git a/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/remote/McpServer-CSharp.http b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/remote/McpServer-CSharp.http new file mode 100644 index 000000000000..f79d8ec14296 --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/remote/McpServer-CSharp.http @@ -0,0 +1,21 @@ +# For more info on HTTP files go to https://aka.ms/vs/httpfile + +#if (hostIdentifier == "vs") +@HostAddress = https://localhost:9995 +#else +@HostAddress = http://localhost:9996 +#endif + +POST {{HostAddress}}/ +Accept: application/json, text/event-stream +Content-Type: application/json +MCP-Protocol-Version: 2025-11-25 + +{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "get_random_number" + } +} diff --git a/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/remote/Program.cs b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/remote/Program.cs new file mode 100644 index 000000000000..47c980c48ab9 --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/remote/Program.cs @@ -0,0 +1,21 @@ +var builder = WebApplication.CreateBuilder(args); + +// Add the MCP services: the transport to use (http) and the tools to register. +builder.Services + .AddMcpServer() + .WithHttpTransport(options => + { + // Stateless mode is recommended for servers that don't need + // server-to-client requests like sampling or elicitation. + // See https://csharp.sdk.modelcontextprotocol.io/concepts/transports/transports.html for details. + options.Stateless = true; + }) + .WithTools(); + +var app = builder.Build(); +app.MapMcp(); +#if (hostIdentifier == "vs") +app.UseHttpsRedirection(); +#endif + +app.Run(); diff --git a/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/remote/Properties/launchSettings.json b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/remote/Properties/launchSettings.json new file mode 100644 index 000000000000..37d5661e4deb --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/remote/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:9996", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:9995;http://localhost:9996", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/remote/README.md b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/remote/README.md new file mode 100644 index 000000000000..dfca0cf7c942 --- /dev/null +++ b/src/ProjectTemplates/McpServer.ProjectTemplates/content/McpServer-CSharp/remote/README.md @@ -0,0 +1,76 @@ +# MCP Server + +This README was created using the C# MCP server project template. +It demonstrates how you can easily create an MCP server using C# and run it as an ASP.NET Core web application. + +#### ---#if (SelfContained) +The MCP server is built as a self-contained application and does not require the .NET runtime to be installed on the target machine. +However, since it is self-contained, it must be built for each target platform separately. +By default, the template is configured to build for: +* `win-x64` +* `win-arm64` +* `osx-arm64` +* `linux-x64` +* `linux-arm64` +* `linux-musl-x64` + +If you require more platforms to be supported, update the list of runtime identifiers in the project's `` element. +#### ---#else +The MCP server is built as a framework-dependent application and requires the ASP.NET Core runtime to be installed on the target machine. +The application is configured to roll-forward to the next highest major version of the runtime if one is available on the target machine. +If an applicable .NET runtime is not available, the MCP server will not start. +Consider building the MCP server as a self-contained application if you want to avoid this dependency. +#### ---#endif + +## Developing locally + +To test this MCP server from source code (locally), you can configure your IDE to connect to the server using localhost. + +#### ---#if (hostIdentifier == "vs") +```json +{ + "servers": { + "McpServer-CSharp": { + "type": "http", + "url": "https://localhost:9995" + } + } +} +``` +#### ---#else +```json +{ + "servers": { + "McpServer-CSharp": { + "type": "http", + "url": "http://localhost:9996" + } + } +} +``` +#### ---#endif + +Refer to the VS Code or Visual Studio documentation for more information on configuring and using MCP servers: + +- [Use MCP servers in VS Code](https://code.visualstudio.com/docs/copilot/chat/mcp-servers) +- [Use MCP servers in Visual Studio](https://learn.microsoft.com/visualstudio/ide/mcp-servers) + +## Testing the MCP Server + +Once configured, 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. + +## Known issues + +1. When using VS Code, connecting to `https://localhost:9995` fails. + * This is related to using a self-signed developer certificate, even when the certificate is trusted by the system. + * Connecting with `http://localhost:9996` succeeds. + * See [Cannot connect to MCP server via SSE using trusted developer certificate (microsoft/vscode#248170)](https://github.com/microsoft/vscode/issues/248170) for more information. + +## More information + +ASP.NET Core MCP servers use the [ModelContextProtocol.AspNetCore](https://www.nuget.org/packages/ModelContextProtocol.AspNetCore) package from the MCP C# SDK. For more information about MCP: + +- [Official Documentation](https://modelcontextprotocol.io/) +- [Protocol Specification](https://spec.modelcontextprotocol.io/) +- [GitHub Organization](https://github.com/modelcontextprotocol) +- [MCP C# SDK](https://csharp.sdk.modelcontextprotocol.io/) diff --git a/src/ProjectTemplates/ProjectTemplates.slnf b/src/ProjectTemplates/ProjectTemplates.slnf index acddb24d68b1..d49256df3ee5 100644 --- a/src/ProjectTemplates/ProjectTemplates.slnf +++ b/src/ProjectTemplates/ProjectTemplates.slnf @@ -64,6 +64,7 @@ "src\\ProjectTemplates\\Web.Client.ItemTemplates\\Microsoft.DotNet.Web.Client.ItemTemplates.csproj", "src\\ProjectTemplates\\Web.ItemTemplates\\Microsoft.DotNet.Web.ItemTemplates.csproj", "src\\ProjectTemplates\\Web.ProjectTemplates\\Microsoft.DotNet.Web.ProjectTemplates.csproj", + "src\\ProjectTemplates\\McpServer.ProjectTemplates\\Microsoft.McpServer.ProjectTemplates.csproj", "src\\ProjectTemplates\\test\\Templates.Blazor.Tests\\Templates.Blazor.Tests.csproj", "src\\ProjectTemplates\\test\\Templates.Blazor.WebAssembly.Auth.Tests\\Templates.Blazor.WebAssembly.Auth.Tests.csproj", "src\\ProjectTemplates\\test\\Templates.Blazor.WebAssembly.Tests\\Templates.Blazor.WebAssembly.Tests.csproj", diff --git a/src/ProjectTemplates/README.md b/src/ProjectTemplates/README.md index 7d34c1a26a24..e661125072de 100644 --- a/src/ProjectTemplates/README.md +++ b/src/ProjectTemplates/README.md @@ -10,6 +10,7 @@ The following contains a description of each sub-directory in the `ProjectTempla - `Web.Client.ItemTemplates`: Contains the Web Client-Side File templates, includes things like less, scss, and typescript - `Web.ItemTemplates`: Contains the Web File templates, includes things like: protobuf, razor component, razor page, view import and start pages - `Web.ProjectTemplates`: Contains the ASP.NET Core Web Template pack, including Blazor Server, WASM, Empty, Grpc, Razor Class Library, RazorPages, MVC, WebApi. +- `McpServer.ProjectTemplates`: Contains the standalone MCP Server Template pack. - `migrations`: Contains migration related scripts. - `scripts`: Contains a collection of scripts that help running tests locally that avoid having to install the templates to the machine. - `test`: Contains the template tests. diff --git a/src/ProjectTemplates/Shared/TemplatePackageInstaller.cs b/src/ProjectTemplates/Shared/TemplatePackageInstaller.cs index def41d4a37aa..480d0aa56336 100644 --- a/src/ProjectTemplates/Shared/TemplatePackageInstaller.cs +++ b/src/ProjectTemplates/Shared/TemplatePackageInstaller.cs @@ -39,6 +39,7 @@ internal static class TemplatePackageInstaller "Microsoft.DotNet.Web.ProjectTemplates.10.0", "Microsoft.DotNet.Web.ProjectTemplates.11.0", "Microsoft.AspNetCore.Blazor.Templates", + "Microsoft.McpServer.ProjectTemplates", }; public static string CustomHivePath { get; } = Path.GetFullPath((string.IsNullOrEmpty(Environment.GetEnvironmentVariable("helix"))) @@ -98,7 +99,7 @@ private static async Task InstallTemplatePackages(ITestOutputHelper output) throw new InvalidOperationException($"Failed to find required templates in {packagesDir}. Please ensure the *Templates*.nupkg have been built."); } - Assert.Equal(3, builtPackages.Length); + Assert.Equal(4, builtPackages.Length); await VerifyCannotFindTemplateAsync(output, "web"); await VerifyCannotFindTemplateAsync(output, "webapp"); diff --git a/src/ProjectTemplates/scripts/Run-McpServer-Locally.ps1 b/src/ProjectTemplates/scripts/Run-McpServer-Locally.ps1 new file mode 100644 index 000000000000..7b8730623b1a --- /dev/null +++ b/src/ProjectTemplates/scripts/Run-McpServer-Locally.ps1 @@ -0,0 +1,12 @@ +#!/usr/bin/env pwsh +#requires -version 4 + +[CmdletBinding(PositionalBinding = $false)] +param() + +Set-StrictMode -Version 2 +$ErrorActionPreference = 'Stop' + +. $PSScriptRoot\Test-Template.ps1 + +Test-Template "mcpserver" "mcpserver" "Microsoft.McpServer.ProjectTemplates.11.0.11.0.0-dev.nupkg" $false diff --git a/src/ProjectTemplates/test/Templates.Tests/McpServerTemplateTest.cs b/src/ProjectTemplates/test/Templates.Tests/McpServerTemplateTest.cs new file mode 100644 index 000000000000..03e8ce0b7dd0 --- /dev/null +++ b/src/ProjectTemplates/test/Templates.Tests/McpServerTemplateTest.cs @@ -0,0 +1,143 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.InternalTesting; +using Templates.Test.Helpers; +using Xunit.Abstractions; + +namespace Templates.Test; + +#pragma warning disable xUnit1041 // Fixture arguments to test classes must have fixture sources + +public class McpServerTemplateTest : LoggedTest +{ + public McpServerTemplateTest(ProjectFactoryFixture projectFactory) + { + ProjectFactory = projectFactory; + } + + public ProjectFactoryFixture ProjectFactory { get; } + private ITestOutputHelper _output; + public ITestOutputHelper Output + { + get + { + if (_output == null) + { + _output = new TestOutputLogger(Logger); + } + return _output; + } + } + + [ConditionalFact] + [SkipOnHelix("Self-contained template requires runtime packages unavailable in CI")] + public async Task McpServerTemplate_Local() + { + await McpServerTemplateCoreAsync("local"); + } + + [ConditionalFact] + [SkipOnHelix("Self-contained template requires runtime packages unavailable in CI")] + public async Task McpServerTemplate_Remote() + { + await McpServerTemplateCoreAsync("remote"); + } + + [ConditionalFact] + public async Task McpServerTemplate_Local_SelfContainedFalse() + { + await McpServerTemplateCoreAsync("local", args: ["--self-contained", "false"]); + } + + [ConditionalFact] + public async Task McpServerTemplate_Remote_SelfContainedFalse() + { + await McpServerTemplateCoreAsync("remote", args: ["--self-contained", "false"]); + } + + [ConditionalFact] + [SkipOnHelix("NativeAOT template requires runtime packages unavailable in CI")] + public async Task McpServerTemplate_Local_NativeAot() + { + await McpServerTemplateCoreAsync("local", args: [ArgConstants.PublishNativeAot]); + } + + [ConditionalFact] + [SkipOnHelix("NativeAOT template requires runtime packages unavailable in CI")] + public async Task McpServerTemplate_Remote_NativeAot() + { + await McpServerTemplateCoreAsync("remote", args: [ArgConstants.PublishNativeAot]); + } + + private async Task McpServerTemplateCoreAsync(string transport, string[] args = null) + { + var nativeAot = args?.Contains(ArgConstants.PublishNativeAot) ?? false; + + var project = await ProjectFactory.CreateProject(Output); + if (nativeAot) + { + project.SetCurrentRuntimeIdentifier(); + } + + var allArgs = new List { "--transport", transport }; + if (args is not null) + { + allArgs.AddRange(args); + } + + await project.RunDotNetNewAsync("mcpserver", args: allArgs.ToArray()); + + if (transport == "remote") + { + var expectedLaunchProfileNames = new[] { "http", "https" }; + await project.VerifyLaunchSettings(expectedLaunchProfileNames); + } + + if (nativeAot) + { + await project.VerifyHasProperty("InvariantGlobalization", "true"); + } + + // Force a restore if native AOT so that RID-specific assets are restored + await project.RunDotNetPublishAsync(noRestore: !nativeAot); + + // Run dotnet build after publish. The reason is that one uses Config = Debug and the other uses Config = Release + // The output from publish will go into bin/Release/netcoreappX.Y/publish and won't be affected by calling build + // later, while the opposite is not true. + await project.RunDotNetBuildAsync(); + + if (transport == "local") + { + using (var aspNetProcess = project.StartBuiltProjectAsync(hasListeningUri: false)) + { + Assert.False( + aspNetProcess.Process.HasExited, + ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", project, aspNetProcess.Process)); + } + + using (var aspNetProcess = project.StartPublishedProjectAsync(hasListeningUri: false, usePublishedAppHost: nativeAot)) + { + Assert.False( + aspNetProcess.Process.HasExited, + ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", project, aspNetProcess.Process)); + } + } + else + { + using (var aspNetProcess = project.StartBuiltProjectAsync(hasListeningUri: true)) + { + Assert.False( + aspNetProcess.Process.HasExited, + ErrorMessages.GetFailedProcessMessageOrEmpty("Run built project", project, aspNetProcess.Process)); + } + + using (var aspNetProcess = project.StartPublishedProjectAsync(hasListeningUri: true, usePublishedAppHost: nativeAot)) + { + Assert.False( + aspNetProcess.Process.HasExited, + ErrorMessages.GetFailedProcessMessageOrEmpty("Run published project", project, aspNetProcess.Process)); + } + } + } +} diff --git a/src/ProjectTemplates/test/Templates.Tests/Templates.Tests.csproj b/src/ProjectTemplates/test/Templates.Tests/Templates.Tests.csproj index 28f6c92fe185..fc55178f5d62 100644 --- a/src/ProjectTemplates/test/Templates.Tests/Templates.Tests.csproj +++ b/src/ProjectTemplates/test/Templates.Tests/Templates.Tests.csproj @@ -39,6 +39,7 @@ + @@ -70,6 +71,10 @@ Private="false" ReferenceOutputAssembly="false" SkipGetTargetFrameworkProperties="true" /> + diff --git a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json index 3e8d66e5af62..154c905637f3 100644 --- a/src/ProjectTemplates/test/Templates.Tests/template-baselines.json +++ b/src/ProjectTemplates/test/Templates.Tests/template-baselines.json @@ -3767,5 +3767,28 @@ "wwwroot/{ProjectName}.lib.module.js" ] } + }, + "mcpserver": { + "Local": { + "Template": "mcpserver", + "Arguments": "new mcpserver --transport local", + "Files": [ + "Program.cs", + "README.md", + ".mcp/server.json", + "Tools/RandomNumberTools.cs" + ] + }, + "Remote": { + "Template": "mcpserver", + "Arguments": "new mcpserver --transport remote", + "Files": [ + "Program.cs", + "README.md", + "{ProjectName}.http", + "Properties/launchSettings.json", + "Tools/RandomNumberTools.cs" + ] + } } }