diff --git a/Directory.Packages.props b/Directory.Packages.props index 1c15c8941..554361cbe 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -25,7 +25,7 @@ - + diff --git a/ModelContextProtocol.sln b/ModelContextProtocol.sln index 4d2b4b5a7..1f7a475b6 100644 --- a/ModelContextProtocol.sln +++ b/ModelContextProtocol.sln @@ -46,6 +46,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatWithTools", "samples\ChatWithTools\ChatWithTools.csproj", "{0C6D0512-D26D-63D3-5019-C5F7A657B28C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickstartWeatherServer", "samples\QuickstartWeatherServer\QuickstartWeatherServer.csproj", "{4653EB0C-8FC0-98F4-E9C8-220EDA7A69DF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickstartClient", "samples\QuickstartClient\QuickstartClient.csproj", "{0D1552DC-E6ED-4AAC-5562-12F8352F46AA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -80,6 +84,14 @@ Global {0C6D0512-D26D-63D3-5019-C5F7A657B28C}.Debug|Any CPU.Build.0 = Debug|Any CPU {0C6D0512-D26D-63D3-5019-C5F7A657B28C}.Release|Any CPU.ActiveCfg = Release|Any CPU {0C6D0512-D26D-63D3-5019-C5F7A657B28C}.Release|Any CPU.Build.0 = Release|Any CPU + {4653EB0C-8FC0-98F4-E9C8-220EDA7A69DF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4653EB0C-8FC0-98F4-E9C8-220EDA7A69DF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4653EB0C-8FC0-98F4-E9C8-220EDA7A69DF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4653EB0C-8FC0-98F4-E9C8-220EDA7A69DF}.Release|Any CPU.Build.0 = Release|Any CPU + {0D1552DC-E6ED-4AAC-5562-12F8352F46AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D1552DC-E6ED-4AAC-5562-12F8352F46AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D1552DC-E6ED-4AAC-5562-12F8352F46AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D1552DC-E6ED-4AAC-5562-12F8352F46AA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -93,6 +105,8 @@ Global {B6F42305-423F-56FF-090F-B7263547F924} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} {20AACB9B-307D-419C-BCC6-1C639C402295} = {1288ADA5-1BF1-4A7F-A33E-9EA29097AA40} {0C6D0512-D26D-63D3-5019-C5F7A657B28C} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {4653EB0C-8FC0-98F4-E9C8-220EDA7A69DF} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} + {0D1552DC-E6ED-4AAC-5562-12F8352F46AA} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {384A3888-751F-4D75-9AE5-587330582D89} diff --git a/samples/QuickstartClient/Program.cs b/samples/QuickstartClient/Program.cs new file mode 100644 index 000000000..364c2b870 --- /dev/null +++ b/samples/QuickstartClient/Program.cs @@ -0,0 +1,98 @@ +using Anthropic.SDK; +using Microsoft.Extensions.AI; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol.Transport; + +var builder = Host.CreateEmptyApplicationBuilder(settings: null); + +builder.Configuration + .AddEnvironmentVariables() + .AddUserSecrets(); + +var (command, arguments) = GetCommandAndArguments(args); + +await using var mcpClient = await McpClientFactory.CreateAsync(new() +{ + Id = "demo-server", + Name = "Demo Server", + TransportType = TransportTypes.StdIo, + TransportOptions = new() + { + ["command"] = command, + ["arguments"] = arguments, + } +}); + +var tools = await mcpClient.ListToolsAsync(); +foreach (var tool in tools) +{ + Console.WriteLine($"Connected to server with tools: {tool.Name}"); +} + +using var anthropicClient = new AnthropicClient(new APIAuthentication(builder.Configuration["ANTHROPIC_API_KEY"])) + .Messages + .AsBuilder() + .UseFunctionInvocation() + .Build(); + +var options = new ChatOptions +{ + MaxOutputTokens = 1000, + ModelId = "claude-3-5-sonnet-20241022", + Tools = [.. tools] +}; + +Console.ForegroundColor = ConsoleColor.Green; +Console.WriteLine("MCP Client Started!"); +Console.ResetColor(); + +PromptForInput(); +while(Console.ReadLine() is string query && !"exit".Equals(query, StringComparison.OrdinalIgnoreCase)) +{ + if (string.IsNullOrWhiteSpace(query)) + { + PromptForInput(); + continue; + } + + await foreach (var message in anthropicClient.GetStreamingResponseAsync(query, options)) + { + Console.Write(message); + } + Console.WriteLine(); + + PromptForInput(); +} + +static void PromptForInput() +{ + Console.WriteLine("Enter a command (or 'exit' to quit):"); + Console.ForegroundColor = ConsoleColor.Cyan; + Console.Write("> "); + Console.ResetColor(); +} + +/// +/// Determines the command (executable) to run and the script/path to pass to it. This allows different +/// languages/runtime environments to be used as the MCP server. +/// +/// +/// This method uses the file extension of the first argument to determine the command, if it's py, it'll run python, +/// if it's js, it'll run node, if it's a directory or a csproj file, it'll run dotnet. +/// +/// If no arguments are provided, it defaults to running the QuickstartWeatherServer project from the current repo. +/// +/// This method would only be required if you're creating a generic client, such as we use for the quickstart. +/// +static (string command, string arguments) GetCommandAndArguments(string[] args) +{ + return args switch + { + [var script] when script.EndsWith(".py") => ("python", script), + [var script] when script.EndsWith(".js") => ("node", script), + [var script] when Directory.Exists(script) || (File.Exists(script) && script.EndsWith(".csproj")) => ("dotnet", $"run --project {script} --no-build"), + _ => ("dotnet", "run --project ../../../../QuickstartWeatherServer --no-build") + }; +} \ No newline at end of file diff --git a/samples/QuickstartClient/QuickstartClient.csproj b/samples/QuickstartClient/QuickstartClient.csproj new file mode 100644 index 000000000..b820bedc1 --- /dev/null +++ b/samples/QuickstartClient/QuickstartClient.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + enable + enable + a4e20a70-5009-4b81-b5b6-780b6d43e78e + + + + + + + + + + + + diff --git a/samples/QuickstartWeatherServer/Program.cs b/samples/QuickstartWeatherServer/Program.cs new file mode 100644 index 000000000..fbc2b44bb --- /dev/null +++ b/samples/QuickstartWeatherServer/Program.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using ModelContextProtocol; +using System.Net.Http.Headers; + +var builder = Host.CreateEmptyApplicationBuilder(settings: null); + +builder.Services.AddMcpServer() + .WithStdioServerTransport() + .WithToolsFromAssembly(); + +builder.Services.AddSingleton(_ => +{ + var client = new HttpClient() { BaseAddress = new Uri("https://api.weather.gov") }; + client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0")); + return client; +}); + +await builder.Build().RunAsync(); diff --git a/samples/QuickstartWeatherServer/QuickstartWeatherServer.csproj b/samples/QuickstartWeatherServer/QuickstartWeatherServer.csproj new file mode 100644 index 000000000..2e9154fd2 --- /dev/null +++ b/samples/QuickstartWeatherServer/QuickstartWeatherServer.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/samples/QuickstartWeatherServer/Tools/WeatherTools.cs b/samples/QuickstartWeatherServer/Tools/WeatherTools.cs new file mode 100644 index 000000000..697b80952 --- /dev/null +++ b/samples/QuickstartWeatherServer/Tools/WeatherTools.cs @@ -0,0 +1,53 @@ +using ModelContextProtocol.Server; +using System.ComponentModel; +using System.Net.Http.Json; +using System.Text.Json; + +namespace QuickstartWeatherServer.Tools; + +[McpServerToolType] +public static class WeatherTools +{ + [McpServerTool, Description("Get weather alerts for a US state.")] + public static async Task GetAlerts( + HttpClient client, + [Description("The US state to get alerts for.")] string state) + { + var jsonElement = await client.GetFromJsonAsync($"/alerts/active/area/{state}"); + var alerts = jsonElement.GetProperty("features").EnumerateArray(); + + if (!alerts.Any()) + { + return "No active alerts for this state."; + } + + return string.Join("\n--\n", alerts.Select(alert => + { + JsonElement properties = alert.GetProperty("properties"); + return $""" + Event: {properties.GetProperty("event").GetString()} + Area: {properties.GetProperty("areaDesc").GetString()} + Severity: {properties.GetProperty("severity").GetString()} + Description: {properties.GetProperty("description").GetString()} + Instruction: {properties.GetProperty("instruction").GetString()} + """; + })); + } + + [McpServerTool, Description("Get weather forecast for a location.")] + public static async Task GetForecast( + HttpClient client, + [Description("Latitude of the location.")] double latitude, + [Description("Longitude of the location.")] double longitude) + { + var jsonElement = await client.GetFromJsonAsync($"/points/{latitude},{longitude}"); + var periods = jsonElement.GetProperty("properties").GetProperty("periods").EnumerateArray(); + + return string.Join("\n---\n", periods.Select(period => $""" + {period.GetProperty("name").GetString()} + Temperature: {period.GetProperty("temperature").GetInt32()}°F + Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()} + Forecast: {period.GetProperty("detailedForecast").GetString()} + """)); + } +} \ No newline at end of file