diff --git a/ModelContextProtocol.slnx b/ModelContextProtocol.slnx index 1f6dce1ed..3ad349d47 100644 --- a/ModelContextProtocol.slnx +++ b/ModelContextProtocol.slnx @@ -47,7 +47,6 @@ - diff --git a/samples/FileBasedMcpServer/Program.cs b/samples/FileBasedMcpServer/Program.cs deleted file mode 100755 index daf9464d1..000000000 --- a/samples/FileBasedMcpServer/Program.cs +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env -S dotnet run -- -#:package Microsoft.Extensions.Hosting -#:project ../../src/ModelContextProtocol/ModelContextProtocol.csproj - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using ModelContextProtocol.Server; -using System.ComponentModel; - -var builder = Host.CreateApplicationBuilder(args); - -builder.Services.AddMcpServer() - .WithStdioServerTransport() - .WithTools(); - -builder.Logging.AddConsole(options => -{ - options.LogToStandardErrorThreshold = LogLevel.Trace; -}); - -await builder.Build().RunAsync(); - -// File-scoped tool class -[McpServerToolType] -file class EchoTool -{ - [McpServerTool(Name = "echo"), Description("Echoes the message back to the client.")] - public static string Echo([Description("The message to echo back.")] string message) => $"Echo: {message}"; -} diff --git a/samples/FileBasedMcpServer/README.md b/samples/FileBasedMcpServer/README.md deleted file mode 100644 index b28c9298d..000000000 --- a/samples/FileBasedMcpServer/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# File-Based MCP Server Sample - -This sample demonstrates how to create a complete MCP (Model Context Protocol) server using .NET 10's file-based programs feature. Unlike traditional .NET projects that require a `.csproj` file, file-based programs allow you to write and run complete applications in a single `.cs` file. - -## Requirements - -- .NET 10 SDK (RC2 or later) -- No project file required! - -## Running the Sample - -Simply run the Program.cs file directly: - -```bash -dotnet run Program.cs -``` - -The server will start and listen for MCP messages on stdin/stdout (stdio transport). - -### Making it Executable (Unix/Linux/macOS) - -On Unix-like systems, you can make the file executable: - -```bash -chmod +x Program.cs -./Program.cs -``` - -Note: The shebang line uses `/usr/bin/env` to locate `dotnet`, so ensure it's in your PATH. - -## Testing the Server - -You can test the server by using `@modelcontextprotocol/inspector`, any stdio-compatible client, or sending JSON-RPC messages to stdin. Here's an example: - -### Initialize the server: -```bash -echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0"}}}' | dotnet run Program.cs -``` - -### List available tools: -```bash -( - echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0"}}}' - sleep 0.5 - echo '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}' - sleep 1 -) | dotnet run Program.cs 2>/dev/null | grep '^{' | jq . -``` - -### Call the echo tool: -```bash -( - echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0"}}}' - sleep 0.5 - echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"message":"Hello, MCP!"}}}' - sleep 1 -) | dotnet run Program.cs 2>/dev/null | grep '^{' | jq . -``` - -## Reference - -- [File-Based Programs Tutorial](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/tutorials/file-based-programs) -- [C# Preprocessor Directives for File-Based Apps](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives#file-based-apps) -- [Model Context Protocol Specification](https://modelcontextprotocol.io/specification/) diff --git a/samples/InMemoryTransport/Program.cs b/samples/InMemoryTransport/Program.cs index dbffaa34d..408ff44b4 100644 --- a/samples/InMemoryTransport/Program.cs +++ b/samples/InMemoryTransport/Program.cs @@ -1,4 +1,4 @@ -using ModelContextProtocol.Client; +using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; using System.IO.Pipelines; diff --git a/samples/QuickstartWeatherServer/Program.cs b/samples/QuickstartWeatherServer/Program.cs old mode 100644 new mode 100755 index 9bc050b54..92ebdd02f --- a/samples/QuickstartWeatherServer/Program.cs +++ b/samples/QuickstartWeatherServer/Program.cs @@ -1,8 +1,16 @@ +#!/usr/bin/env -S dotnet run -- +#:package Microsoft.Extensions.Hosting +#:project ../../src/ModelContextProtocol/ModelContextProtocol.csproj + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using QuickstartWeatherServer.Tools; +using ModelContextProtocol; +using ModelContextProtocol.Server; +using System.ComponentModel; +using System.Globalization; using System.Net.Http.Headers; +using System.Text.Json; var builder = Host.CreateApplicationBuilder(args); @@ -20,3 +28,68 @@ builder.Services.AddSingleton(httpClient); await builder.Build().RunAsync(); + +// Weather Tools +[McpServerToolType] +file sealed 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. Use the 2 letter abbreviation for the state (e.g. NY).")] string state) + { + using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}"); + var jsonElement = jsonDocument.RootElement; + 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 pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}"); + using var locationDocument = await client.ReadJsonDocumentAsync(pointUrl); + var forecastUrl = locationDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString() + ?? throw new McpException($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}"); + + using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl); + var periods = forecastDocument.RootElement.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()} + """)); + } +} + +// HttpClient Extension Methods +file static class HttpClientExt +{ + public static async Task ReadJsonDocumentAsync(this HttpClient client, string requestUri) + { + using var response = await client.GetAsync(requestUri); + response.EnsureSuccessStatusCode(); + return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); + } +} diff --git a/samples/QuickstartWeatherServer/QuickstartWeatherServer.csproj b/samples/QuickstartWeatherServer/QuickstartWeatherServer.csproj deleted file mode 100644 index dc1108a8f..000000000 --- a/samples/QuickstartWeatherServer/QuickstartWeatherServer.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - Exe - net8.0 - enable - enable - true - - - - - - - - - - - diff --git a/samples/QuickstartWeatherServer/README.md b/samples/QuickstartWeatherServer/README.md new file mode 100644 index 000000000..4ca494d6d --- /dev/null +++ b/samples/QuickstartWeatherServer/README.md @@ -0,0 +1,61 @@ +# QuickstartWeatherServer Sample + +This sample demonstrates how to create an MCP server that provides weather-related tools using the weather.gov API. This is a file-based program that runs without a traditional project file. + +## Requirements + +- .NET 8.0 SDK or later +- No project file required! + +## Running the Sample + +Simply run the Program.cs file directly: + +```bash +dotnet run Program.cs +``` + +Or on Unix-like systems, make the file executable: + +```bash +chmod +x Program.cs +./Program.cs +``` + +The server will start and listen for MCP messages on stdin/stdout (stdio transport). + +## Available Tools + +The server provides two weather tools: + +1. **GetAlerts** - Get weather alerts for a US state (use 2-letter abbreviation like "NY") +2. **GetForecast** - Get weather forecast for a location (requires latitude and longitude) + +## Testing the Server + +You can test the server using the QuickstartClient or any MCP-compatible client: + +```bash +# From the repository root +dotnet run --project samples/QuickstartClient samples/QuickstartWeatherServer +``` + +Or test with the MCP inspector: + +```bash +npx @modelcontextprotocol/inspector dotnet run Program.cs +``` + +## What This Sample Shows + +- Creating an MCP server using `Host.CreateApplicationBuilder` +- Registering tools with `WithTools()` +- Using dependency injection to provide HttpClient to tools +- Configuring logging to stderr for MCP compatibility +- Using file-scoped classes for tool implementations + +## Reference + +- [File-Based Programs Tutorial](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/tutorials/file-based-programs) +- [Model Context Protocol Specification](https://modelcontextprotocol.io/specification/) +- [Weather.gov API](https://www.weather.gov/documentation/services-web-api) diff --git a/samples/QuickstartWeatherServer/Tools/HttpClientExt.cs b/samples/QuickstartWeatherServer/Tools/HttpClientExt.cs deleted file mode 100644 index f7b2b5499..000000000 --- a/samples/QuickstartWeatherServer/Tools/HttpClientExt.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Text.Json; - -namespace ModelContextProtocol; - -internal static class HttpClientExt -{ - public static async Task ReadJsonDocumentAsync(this HttpClient client, string requestUri) - { - using var response = await client.GetAsync(requestUri); - response.EnsureSuccessStatusCode(); - return await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync()); - } -} \ No newline at end of file diff --git a/samples/QuickstartWeatherServer/Tools/WeatherTools.cs b/samples/QuickstartWeatherServer/Tools/WeatherTools.cs deleted file mode 100644 index 61dc0a0ee..000000000 --- a/samples/QuickstartWeatherServer/Tools/WeatherTools.cs +++ /dev/null @@ -1,60 +0,0 @@ -using ModelContextProtocol; -using ModelContextProtocol.Server; -using System.ComponentModel; -using System.Globalization; -using System.Text.Json; - -namespace QuickstartWeatherServer.Tools; - -[McpServerToolType] -public sealed 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. Use the 2 letter abbreviation for the state (e.g. NY).")] string state) - { - using var jsonDocument = await client.ReadJsonDocumentAsync($"/alerts/active/area/{state}"); - var jsonElement = jsonDocument.RootElement; - 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 pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}"); - using var locationDocument = await client.ReadJsonDocumentAsync(pointUrl); - var forecastUrl = locationDocument.RootElement.GetProperty("properties").GetProperty("forecast").GetString() - ?? throw new McpException($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}"); - - using var forecastDocument = await client.ReadJsonDocumentAsync(forecastUrl); - var periods = forecastDocument.RootElement.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()} - """)); - } -}